fix: tombstone/deletion overhaul for bd-4q8

- IsExpired(): Negative TTL means immediately expired (for --hard mode)
- IsExpired(): ClockSkewGrace only added for TTLs > 1 hour
- bd cleanup --hard: Use negative TTL to prune freshly created tombstones
- bd delete --hard: New flag to immediately prune tombstones from JSONL
- Import: Add early tombstone check before all phases to prevent resurrection

The early tombstone check prevents ghost issues from being created when
tombstones exist in the DB. However, a deeper git merge issue (bd-ncwo)
can still cause resurrection when remote's status:closed wins the merge.

🤖 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:01:31 -08:00
parent 8d73a86f7a
commit 0ce039429d
5 changed files with 95 additions and 12 deletions

View File

@@ -98,7 +98,10 @@ func (i *Issue) IsTombstone() bool {
// IsExpired returns true if the tombstone has exceeded its TTL.
// Non-tombstone issues always return false.
// ttl is the configured TTL duration; if zero, DefaultTombstoneTTL is used.
// ttl is the configured TTL duration:
// - If zero, DefaultTombstoneTTL (30 days) is used
// - If negative, the tombstone is immediately expired (for --hard mode)
// - If positive, ClockSkewGrace is added only for TTLs > 1 hour
func (i *Issue) IsExpired(ttl time.Duration) bool {
// Non-tombstones never expire
if !i.IsTombstone() {
@@ -110,13 +113,22 @@ func (i *Issue) IsExpired(ttl time.Duration) bool {
return false
}
// Negative TTL means "immediately expired" - for --hard mode (bd-4q8 fix)
if ttl < 0 {
return true
}
// Use default TTL if not specified
if ttl == 0 {
ttl = DefaultTombstoneTTL
}
// Add clock skew grace period to the TTL
effectiveTTL := ttl + ClockSkewGrace
// Only add clock skew grace period for normal TTLs (> 1 hour).
// For short TTLs (testing/development), skip grace period.
effectiveTTL := ttl
if ttl > ClockSkewGrace {
effectiveTTL = ttl + ClockSkewGrace
}
// Check if the tombstone has exceeded its TTL
expirationTime := i.DeletedAt.Add(effectiveTTL)

View File

@@ -756,6 +756,31 @@ func TestIsExpired(t *testing.T) {
ttl: 7 * 24 * time.Hour,
expired: false,
},
{
name: "negative TTL means immediately expired (bd-4q8 --hard mode)",
issue: Issue{
ID: "test-14",
Title: "(deleted)",
Status: StatusTombstone,
Priority: 0,
IssueType: TypeTask,
DeletedAt: timePtr(now), // Just deleted NOW
},
ttl: -1, // Negative TTL = immediate expiration
expired: true,
},
{
name: "non-tombstone never expires even with negative TTL",
issue: Issue{
ID: "test-15",
Title: "Open issue",
Status: StatusOpen,
Priority: 0,
IssueType: TypeTask,
},
ttl: -1,
expired: false,
},
}
for _, tt := range tests {