fix: Replace mtime-based auto-import with hash-based comparison (bd-84)

The auto-import mechanism previously relied on file modification time
comparison between JSONL and DB. This broke in git workflows because
git doesn't preserve original mtimes - pulled files get fresh timestamps.

Changes:
- Added metadata table for internal state storage (separate from config)
- Replaced mtime comparison with SHA256 hash comparison in autoImportIfNewer()
- Store JSONL hash in metadata after both import and export operations
- Added crypto/sha256 and encoding/hex imports

Benefits:
- Git-proof: Works regardless of file timestamps after git pull
- Universal: Works with git, Dropbox, rsync, manual edits
- Efficient: SHA256 is fast (~20ms for 1MB files)
- Accurate: Only imports when content actually changed
- No user action required: Fully automatic and invisible

Testing:
- All existing tests pass
- Manual testing confirms hash-based import triggers on content changes
- Linter warnings are baseline only (documented in LINTING.md)

This fixes issues where parallel agents in git workflows couldn't
find their assigned issues after git pull because auto-import
silently failed due to stale mtimes.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-10-14 02:54:57 -07:00
parent 2bd0f11698
commit ea5157e204

View File

@@ -3,6 +3,8 @@ package main
import (
"bufio"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
@@ -152,13 +154,14 @@ func findJSONLPath() string {
return jsonlPath
}
// autoImportIfNewer checks if JSONL is newer than DB and imports if so
// autoImportIfNewer checks if JSONL content changed (via hash) and imports if so
// Fixes bd-84: Hash-based comparison is git-proof (mtime comparison fails after git pull)
func autoImportIfNewer() {
// Find JSONL path
jsonlPath := findJSONLPath()
// Check if JSONL exists
jsonlInfo, err := os.Stat(jsonlPath)
// Read JSONL file
jsonlData, err := os.ReadFile(jsonlPath)
if err != nil {
// JSONL doesn't exist or can't be accessed, skip import
if os.Getenv("BD_DEBUG") != "" {
@@ -167,34 +170,38 @@ func autoImportIfNewer() {
return
}
// Check if DB exists
dbInfo, err := os.Stat(dbPath)
// Compute current JSONL hash
hasher := sha256.New()
hasher.Write(jsonlData)
currentHash := hex.EncodeToString(hasher.Sum(nil))
// Get last import hash from DB metadata
ctx := context.Background()
lastHash, err := store.GetMetadata(ctx, "last_import_hash")
if err != nil {
// DB doesn't exist (new init?), skip import
// Metadata not supported or error reading - this shouldn't happen
// since we added metadata table, but be defensive
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: auto-import skipped, DB not found: %v\n", err)
fmt.Fprintf(os.Stderr, "Debug: auto-import skipped, metadata error: %v\n", err)
}
return
}
// Compare modification times
if !jsonlInfo.ModTime().After(dbInfo.ModTime()) {
// JSONL is not newer than DB, skip import
// Compare hashes
if currentHash == lastHash {
// Content unchanged, skip import
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: auto-import skipped, JSONL unchanged (hash match)\n")
}
return
}
// JSONL is newer, perform silent import
ctx := context.Background()
// Read and parse JSONL
f, err := os.Open(jsonlPath)
if err != nil {
// Can't open JSONL, skip import
return
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: auto-import triggered (hash changed)\n")
}
defer f.Close()
scanner := bufio.NewScanner(f)
// Content changed - perform silent import
scanner := bufio.NewScanner(strings.NewReader(string(jsonlData)))
var allIssues []*types.Issue
for scanner.Scan() {
@@ -206,6 +213,9 @@ func autoImportIfNewer() {
var issue types.Issue
if err := json.Unmarshal([]byte(line), &issue); err != nil {
// Parse error, skip this import
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: auto-import skipped, parse error: %v\n", err)
}
return
}
@@ -276,6 +286,9 @@ func autoImportIfNewer() {
}
}
}
// Store new hash after successful import
_ = store.SetMetadata(ctx, "last_import_hash", currentHash)
}
// markDirtyAndScheduleFlush marks the database as dirty and schedules a flush
@@ -484,6 +497,15 @@ func flushToJSONL() {
fmt.Fprintf(os.Stderr, "Warning: failed to clear dirty issues: %v\n", err)
}
// Store hash of exported JSONL (fixes bd-84: enables hash-based auto-import)
jsonlData, err := os.ReadFile(jsonlPath)
if err == nil {
hasher := sha256.New()
hasher.Write(jsonlData)
exportedHash := hex.EncodeToString(hasher.Sum(nil))
_ = store.SetMetadata(ctx, "last_import_hash", exportedHash)
}
// Success!
recordSuccess()
}