feat(sync): add sync mode configuration (hq-ew1mbr.3)
Add configurable sync modes for Dolt storage integration: Sync modes: - git-portable (default): Export JSONL on push, import on pull - realtime: Export JSONL on every database change - dolt-native: Use Dolt remotes directly (no JSONL) - belt-and-suspenders: Both Dolt remote AND JSONL backup Configuration options in .beads/config.yaml: - sync.mode: Select sync mode - sync.export_on: push (default) or change - sync.import_on: pull (default) or change - conflict.strategy: newest (default), ours, theirs, manual - federation.remote: Dolt remote URL for dolt-native mode - federation.sovereignty: T1-T4 data sovereignty tier The sync command now displays configuration in `bd sync --status` and uses configured conflict strategy for resolution. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,67 @@ import (
|
||||
"github.com/steveyegge/beads/internal/debug"
|
||||
)
|
||||
|
||||
// Sync mode constants define how beads syncs with git/remotes.
|
||||
const (
|
||||
// SyncModeGitPortable exports JSONL on push, imports on pull (default).
|
||||
// This is the standard git-based workflow where JSONL is committed.
|
||||
SyncModeGitPortable = "git-portable"
|
||||
|
||||
// SyncModeRealtime exports JSONL on every database change.
|
||||
// Legacy behavior, more frequent writes but higher I/O.
|
||||
SyncModeRealtime = "realtime"
|
||||
|
||||
// SyncModeDoltNative uses Dolt remotes directly (dolthub://, gs://, s3://).
|
||||
// No JSONL export needed - Dolt handles sync.
|
||||
SyncModeDoltNative = "dolt-native"
|
||||
|
||||
// SyncModeBeltAndSuspenders uses both Dolt remote AND JSONL backup.
|
||||
// Maximum redundancy for critical data.
|
||||
SyncModeBeltAndSuspenders = "belt-and-suspenders"
|
||||
)
|
||||
|
||||
// Sync trigger constants define when sync operations occur.
|
||||
const (
|
||||
// SyncTriggerPush triggers sync on git push operations.
|
||||
SyncTriggerPush = "push"
|
||||
|
||||
// SyncTriggerChange triggers sync on every database change.
|
||||
SyncTriggerChange = "change"
|
||||
|
||||
// SyncTriggerPull triggers import on git pull operations.
|
||||
SyncTriggerPull = "pull"
|
||||
)
|
||||
|
||||
// Conflict strategy constants define how sync conflicts are resolved.
|
||||
const (
|
||||
// ConflictStrategyNewest keeps whichever version has the newer updated_at timestamp.
|
||||
ConflictStrategyNewest = "newest"
|
||||
|
||||
// ConflictStrategyOurs keeps the local version on conflict.
|
||||
ConflictStrategyOurs = "ours"
|
||||
|
||||
// ConflictStrategyTheirs keeps the remote version on conflict.
|
||||
ConflictStrategyTheirs = "theirs"
|
||||
|
||||
// ConflictStrategyManual requires manual resolution of conflicts.
|
||||
ConflictStrategyManual = "manual"
|
||||
)
|
||||
|
||||
// Federation sovereignty tiers define data sovereignty levels.
|
||||
const (
|
||||
// SovereigntyT1 - Full sovereignty: data never leaves controlled infrastructure.
|
||||
SovereigntyT1 = "T1"
|
||||
|
||||
// SovereigntyT2 - Regional sovereignty: data stays within region/jurisdiction.
|
||||
SovereigntyT2 = "T2"
|
||||
|
||||
// SovereigntyT3 - Provider sovereignty: data with trusted cloud provider.
|
||||
SovereigntyT3 = "T3"
|
||||
|
||||
// SovereigntyT4 - No restrictions: data can be anywhere (e.g., DoltHub public).
|
||||
SovereigntyT4 = "T4"
|
||||
)
|
||||
|
||||
var v *viper.Viper
|
||||
|
||||
// Initialize sets up the viper configuration singleton
|
||||
@@ -108,6 +169,19 @@ func Initialize() error {
|
||||
// Sync configuration defaults (bd-4u8)
|
||||
v.SetDefault("sync.require_confirmation_on_mass_delete", false)
|
||||
|
||||
// Sync mode configuration (hq-ew1mbr.3)
|
||||
// See docs/CONFIG.md for detailed documentation
|
||||
v.SetDefault("sync.mode", SyncModeGitPortable) // git-portable | realtime | dolt-native | belt-and-suspenders
|
||||
v.SetDefault("sync.export_on", SyncTriggerPush) // push | change
|
||||
v.SetDefault("sync.import_on", SyncTriggerPull) // pull | change
|
||||
|
||||
// Conflict resolution configuration
|
||||
v.SetDefault("conflict.strategy", ConflictStrategyNewest) // newest | ours | theirs | manual
|
||||
|
||||
// Federation configuration (optional Dolt remote)
|
||||
v.SetDefault("federation.remote", "") // e.g., dolthub://org/beads, gs://bucket/beads, s3://bucket/beads
|
||||
v.SetDefault("federation.sovereignty", "") // T1 | T2 | T3 | T4 (empty = no restriction)
|
||||
|
||||
// Push configuration defaults
|
||||
v.SetDefault("no-push", false)
|
||||
|
||||
@@ -530,3 +604,142 @@ func GetIdentity(flagValue string) string {
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// SyncConfig holds the sync mode configuration.
|
||||
type SyncConfig struct {
|
||||
Mode string // git-portable, realtime, dolt-native, belt-and-suspenders
|
||||
ExportOn string // push, change
|
||||
ImportOn string // pull, change
|
||||
}
|
||||
|
||||
// GetSyncConfig returns the current sync configuration.
|
||||
func GetSyncConfig() SyncConfig {
|
||||
return SyncConfig{
|
||||
Mode: GetSyncMode(),
|
||||
ExportOn: GetString("sync.export_on"),
|
||||
ImportOn: GetString("sync.import_on"),
|
||||
}
|
||||
}
|
||||
|
||||
// GetSyncMode returns the configured sync mode.
|
||||
// Returns git-portable if not configured or invalid.
|
||||
func GetSyncMode() string {
|
||||
mode := GetString("sync.mode")
|
||||
if mode == "" {
|
||||
return SyncModeGitPortable
|
||||
}
|
||||
// Validate mode
|
||||
switch mode {
|
||||
case SyncModeGitPortable, SyncModeRealtime, SyncModeDoltNative, SyncModeBeltAndSuspenders:
|
||||
return mode
|
||||
default:
|
||||
return SyncModeGitPortable
|
||||
}
|
||||
}
|
||||
|
||||
// IsSyncModeValid checks if the given sync mode is valid.
|
||||
func IsSyncModeValid(mode string) bool {
|
||||
switch mode {
|
||||
case SyncModeGitPortable, SyncModeRealtime, SyncModeDoltNative, SyncModeBeltAndSuspenders:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ConflictConfig holds the conflict resolution configuration.
|
||||
type ConflictConfig struct {
|
||||
Strategy string // newest, ours, theirs, manual
|
||||
}
|
||||
|
||||
// GetConflictConfig returns the current conflict resolution configuration.
|
||||
func GetConflictConfig() ConflictConfig {
|
||||
return ConflictConfig{
|
||||
Strategy: GetConflictStrategy(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetConflictStrategy returns the configured conflict resolution strategy.
|
||||
// Returns newest if not configured or invalid.
|
||||
func GetConflictStrategy() string {
|
||||
strategy := GetString("conflict.strategy")
|
||||
if strategy == "" {
|
||||
return ConflictStrategyNewest
|
||||
}
|
||||
// Validate strategy
|
||||
switch strategy {
|
||||
case ConflictStrategyNewest, ConflictStrategyOurs, ConflictStrategyTheirs, ConflictStrategyManual:
|
||||
return strategy
|
||||
default:
|
||||
return ConflictStrategyNewest
|
||||
}
|
||||
}
|
||||
|
||||
// IsConflictStrategyValid checks if the given conflict strategy is valid.
|
||||
func IsConflictStrategyValid(strategy string) bool {
|
||||
switch strategy {
|
||||
case ConflictStrategyNewest, ConflictStrategyOurs, ConflictStrategyTheirs, ConflictStrategyManual:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// FederationConfig holds the federation (Dolt remote) configuration.
|
||||
type FederationConfig struct {
|
||||
Remote string // dolthub://org/beads, gs://bucket/beads, s3://bucket/beads
|
||||
Sovereignty string // T1, T2, T3, T4
|
||||
}
|
||||
|
||||
// GetFederationConfig returns the current federation configuration.
|
||||
func GetFederationConfig() FederationConfig {
|
||||
return FederationConfig{
|
||||
Remote: GetString("federation.remote"),
|
||||
Sovereignty: GetSovereignty(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetSovereignty returns the configured data sovereignty tier.
|
||||
// Returns empty string if not configured.
|
||||
func GetSovereignty() string {
|
||||
sovereignty := GetString("federation.sovereignty")
|
||||
// Validate sovereignty tier
|
||||
switch sovereignty {
|
||||
case SovereigntyT1, SovereigntyT2, SovereigntyT3, SovereigntyT4:
|
||||
return sovereignty
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// IsSovereigntyValid checks if the given sovereignty tier is valid.
|
||||
func IsSovereigntyValid(sovereignty string) bool {
|
||||
switch sovereignty {
|
||||
case "", SovereigntyT1, SovereigntyT2, SovereigntyT3, SovereigntyT4:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldExportOnChange returns true if sync.export_on is set to "change".
|
||||
func ShouldExportOnChange() bool {
|
||||
return GetString("sync.export_on") == SyncTriggerChange
|
||||
}
|
||||
|
||||
// ShouldImportOnChange returns true if sync.import_on is set to "change".
|
||||
func ShouldImportOnChange() bool {
|
||||
return GetString("sync.import_on") == SyncTriggerChange
|
||||
}
|
||||
|
||||
// NeedsDoltRemote returns true if the sync mode requires a Dolt remote.
|
||||
func NeedsDoltRemote() bool {
|
||||
mode := GetSyncMode()
|
||||
return mode == SyncModeDoltNative || mode == SyncModeBeltAndSuspenders
|
||||
}
|
||||
|
||||
// NeedsJSONL returns true if the sync mode requires JSONL export.
|
||||
func NeedsJSONL() bool {
|
||||
mode := GetSyncMode()
|
||||
return mode == SyncModeGitPortable || mode == SyncModeRealtime || mode == SyncModeBeltAndSuspenders
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user