diff --git a/compact.go b/compact.go index a3b5553..17e09e5 100644 --- a/compact.go +++ b/compact.go @@ -12,12 +12,10 @@ import ( var defaultArchiveOutputDirectory = ".core/archive/" -// CompactOptions archives completed journal rows before a cutoff time to a -// compressed JSONL file. -// // Usage example: `options := store.CompactOptions{Before: time.Date(2026, 3, 30, 0, 0, 0, 0, time.UTC), Output: "/tmp/archive", Format: "gzip"}` -// The default output directory is `.core/archive/`; the default format is -// `gzip`, and `zstd` is also supported. +// Usage example: `result := storeInstance.Compact(store.CompactOptions{Before: time.Now().Add(-90 * 24 * time.Hour)})` +// Leave `Output` empty to write gzip JSONL archives under `.core/archive/`, or +// set `Format` to `zstd` when downstream tooling expects `.jsonl.zst`. type CompactOptions struct { // Usage example: `options := store.CompactOptions{Before: time.Now().Add(-90 * 24 * time.Hour)}` Before time.Time @@ -32,7 +30,7 @@ func (compactOptions CompactOptions) Normalised() CompactOptions { if compactOptions.Output == "" { compactOptions.Output = defaultArchiveOutputDirectory } - compactOptions.Format = lowerText(core.Trim(compactOptions.Format)) + compactOptions.Format = lowercaseText(core.Trim(compactOptions.Format)) if compactOptions.Format == "" { compactOptions.Format = "gzip" } @@ -48,7 +46,7 @@ func (compactOptions CompactOptions) Validate() error { nil, ) } - switch lowerText(core.Trim(compactOptions.Format)) { + switch lowercaseText(core.Trim(compactOptions.Format)) { case "", "gzip", "zstd": return nil default: @@ -60,7 +58,7 @@ func (compactOptions CompactOptions) Validate() error { } } -func lowerText(text string) string { +func lowercaseText(text string) string { builder := core.NewBuilder() for _, r := range text { builder.WriteRune(unicode.ToLower(r)) diff --git a/journal.go b/journal.go index ab2e91e..4388241 100644 --- a/journal.go +++ b/journal.go @@ -323,7 +323,7 @@ func parseFluxTime(value string) (time.Time, error) { if value == "" { return time.Time{}, core.E("store.parseFluxTime", "range value is empty", nil) } - value = firstOrEmptyString(core.Split(value, ",")) + value = firstStringOrEmpty(core.Split(value, ",")) value = core.Trim(value) if core.HasPrefix(value, "time(v:") && core.HasSuffix(value, ")") { value = core.Trim(core.TrimSuffix(core.TrimPrefix(value, "time(v:"), ")")) diff --git a/scope.go b/scope.go index 782d7c2..8b46493 100644 --- a/scope.go +++ b/scope.go @@ -60,10 +60,9 @@ func (scopedConfig ScopedStoreConfig) Validate() error { return nil } -// ScopedStore prefixes group names with namespace + ":" before delegating to Store. -// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }; if err := scopedStore.Set("colour", "blue"); err != nil { return }` -// -// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }; if err := scopedStore.SetIn("config", "colour", "blue"); err != nil { return }` +// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }` +// Usage example: `if err := scopedStore.Set("colour", "blue"); err != nil { return } // writes tenant-a:default/colour` +// Usage example: `if err := scopedStore.SetIn("config", "colour", "blue"); err != nil { return } // writes tenant-a:config/colour` type ScopedStore struct { store *Store namespace string @@ -82,10 +81,9 @@ type scopedWatcherBridge struct { done chan struct{} } -// NewScoped validates a namespace and prefixes groups with namespace + ":". // Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }` -// Prefer `NewScopedConfigured` when the namespace and quota are already known -// as a struct literal. +// Prefer `NewScopedConfigured(storeInstance, store.ScopedStoreConfig{Namespace: "tenant-a"})` +// when the namespace and quota are already known at the call site. func NewScoped(storeInstance *Store, namespace string) (*ScopedStore, error) { if storeInstance == nil { return nil, core.E("store.NewScoped", "store instance is nil", nil) @@ -101,8 +99,9 @@ func NewScoped(storeInstance *Store, namespace string) (*ScopedStore, error) { return scopedStore, nil } -// NewScopedConfigured validates the namespace and optional quota settings before constructing a ScopedStore. // Usage example: `scopedStore, err := store.NewScopedConfigured(storeInstance, store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}); if err != nil { return }` +// This keeps the namespace and quota in one declarative literal instead of an +// option chain. func NewScopedConfigured(storeInstance *Store, scopedConfig ScopedStoreConfig) (*ScopedStore, error) { if storeInstance == nil { return nil, core.E("store.NewScopedConfigured", "store instance is nil", nil) @@ -291,7 +290,7 @@ func (scopedStore *ScopedStore) CountAll(groupPrefix ...string) (int, error) { if err := scopedStore.ensureReady("store.CountAll"); err != nil { return 0, err } - return scopedStore.store.CountAll(scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix))) + return scopedStore.store.CountAll(scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix))) } // Usage example: `groupNames, err := scopedStore.Groups("config")` @@ -300,7 +299,7 @@ func (scopedStore *ScopedStore) Groups(groupPrefix ...string) ([]string, error) if err := scopedStore.ensureReady("store.Groups"); err != nil { return nil, err } - groupNames, err := scopedStore.store.Groups(scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix))) + groupNames, err := scopedStore.store.Groups(scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix))) if err != nil { return nil, err } @@ -319,7 +318,7 @@ func (scopedStore *ScopedStore) GroupsSeq(groupPrefix ...string) iter.Seq2[strin return } namespacePrefix := scopedStore.namespacePrefix() - for groupName, err := range scopedStore.store.GroupsSeq(scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix))) { + for groupName, err := range scopedStore.store.GroupsSeq(scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix))) { if err != nil { if !yield("", err) { return @@ -372,8 +371,8 @@ func (scopedStore *ScopedStore) PurgeExpired() (int64, error) { // Usage example: `events := scopedStore.Watch("config")` // Usage example: `events := scopedStore.Watch("*")` -// The returned events always use namespace-local group names, so a write to -// `tenant-a:config` is delivered as `config`. +// A write to `tenant-a:config` is delivered back to this scoped watcher as +// `config`, so callers never have to strip the namespace themselves. func (scopedStore *ScopedStore) Watch(group string) <-chan Event { if scopedStore == nil || scopedStore.store == nil { return closedEventChannel() @@ -479,8 +478,8 @@ func (scopedStore *ScopedStore) localiseWatchedEvent(event Event) (Event, bool) } // Usage example: `unregister := scopedStore.OnChange(func(event store.Event) { fmt.Println(event.Group, event.Key, event.Value) })` -// The callback receives the namespace-local group name, so a write to -// `tenant-a:config` is reported as `config`. +// A callback registered on `tenant-a` receives `config` rather than +// `tenant-a:config`. func (scopedStore *ScopedStore) OnChange(callback func(Event)) func() { if scopedStore == nil || callback == nil { return func() {} @@ -668,7 +667,7 @@ func (scopedStoreTransaction *ScopedStoreTransaction) CountAll(groupPrefix ...st if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.CountAll"); err != nil { return 0, err } - return scopedStoreTransaction.storeTransaction.CountAll(scopedStoreTransaction.scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix))) + return scopedStoreTransaction.storeTransaction.CountAll(scopedStoreTransaction.scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix))) } // Usage example: `groupNames, err := scopedStoreTransaction.Groups("config")` @@ -678,7 +677,7 @@ func (scopedStoreTransaction *ScopedStoreTransaction) Groups(groupPrefix ...stri return nil, err } - groupNames, err := scopedStoreTransaction.storeTransaction.Groups(scopedStoreTransaction.scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix))) + groupNames, err := scopedStoreTransaction.storeTransaction.Groups(scopedStoreTransaction.scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix))) if err != nil { return nil, err } @@ -698,7 +697,7 @@ func (scopedStoreTransaction *ScopedStoreTransaction) GroupsSeq(groupPrefix ...s } namespacePrefix := scopedStoreTransaction.scopedStore.namespacePrefix() - for groupName, err := range scopedStoreTransaction.storeTransaction.GroupsSeq(scopedStoreTransaction.scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix))) { + for groupName, err := range scopedStoreTransaction.storeTransaction.GroupsSeq(scopedStoreTransaction.scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix))) { if err != nil { if !yield("", err) { return diff --git a/store.go b/store.go index 8781821..a8448df 100644 --- a/store.go +++ b/store.go @@ -763,7 +763,7 @@ func (storeInstance *Store) Groups(groupPrefix ...string) ([]string, error) { // Usage example: `for tenantGroupName, err := range storeInstance.GroupsSeq("tenant-a:") { if err != nil { break }; fmt.Println(tenantGroupName) }` // Usage example: `for groupName, err := range storeInstance.GroupsSeq() { if err != nil { break }; fmt.Println(groupName) }` func (storeInstance *Store) GroupsSeq(groupPrefix ...string) iter.Seq2[string, error] { - actualGroupPrefix := firstOrEmptyString(groupPrefix) + actualGroupPrefix := firstStringOrEmpty(groupPrefix) return func(yield func(string, error) bool) { if err := storeInstance.ensureReady("store.GroupsSeq"); err != nil { yield("", err) @@ -808,7 +808,7 @@ func (storeInstance *Store) GroupsSeq(groupPrefix ...string) iter.Seq2[string, e } } -func firstOrEmptyString(values []string) string { +func firstStringOrEmpty(values []string) string { if len(values) == 0 { return "" } diff --git a/transaction.go b/transaction.go index 285dde7..2b730f5 100644 --- a/transaction.go +++ b/transaction.go @@ -386,7 +386,7 @@ func (storeTransaction *StoreTransaction) Groups(groupPrefix ...string) ([]strin // Usage example: `for groupName, err := range transaction.GroupsSeq("tenant-a:") { if err != nil { break }; fmt.Println(groupName) }` // Usage example: `for groupName, err := range transaction.GroupsSeq() { if err != nil { break }; fmt.Println(groupName) }` func (storeTransaction *StoreTransaction) GroupsSeq(groupPrefix ...string) iter.Seq2[string, error] { - actualGroupPrefix := firstOrEmptyString(groupPrefix) + actualGroupPrefix := firstStringOrEmpty(groupPrefix) return func(yield func(string, error) bool) { if err := storeTransaction.ensureReady("store.Transaction.GroupsSeq"); err != nil { yield("", err) diff --git a/workspace.go b/workspace.go index 3cab19a..4f428fe 100644 --- a/workspace.go +++ b/workspace.go @@ -33,12 +33,11 @@ FROM workspace_entries` var defaultWorkspaceStateDirectory = ".core/state/" -// Workspace keeps mutable work-in-progress in a SQLite file such as -// `.core/state/scroll-session.duckdb` until Commit() or Discard() removes it. -// // Usage example: `workspace, err := storeInstance.NewWorkspace("scroll-session"); if err != nil { return }; defer workspace.Discard()` -// // Usage example: `workspace, err := storeInstance.NewWorkspace("scroll-session-2026-03-30"); if err != nil { return }; defer workspace.Discard(); _ = workspace.Put("like", map[string]any{"user": "@alice"})` +// Each workspace keeps mutable work-in-progress in a SQLite file such as +// `.core/state/scroll-session.duckdb` until `Commit()` or `Discard()` removes +// it. type Workspace struct { name string store *Store @@ -67,11 +66,10 @@ func (workspace *Workspace) DatabasePath() string { return workspace.databasePath } -// Close keeps the workspace file on disk so `RecoverOrphans(".core/state/")` -// can reopen it later. -// // Usage example: `if err := workspace.Close(); err != nil { return }` // Usage example: `if err := workspace.Close(); err != nil { return }; orphans := storeInstance.RecoverOrphans(".core/state"); _ = orphans` +// `Close()` keeps the `.duckdb` file on disk so `RecoverOrphans(".core/state")` +// can reopen it after a crash or interrupted agent run. func (workspace *Workspace) Close() error { return workspace.closeWithoutRemovingFiles() } @@ -103,11 +101,9 @@ func (workspace *Workspace) ensureReady(operation string) error { return nil } -// NewWorkspace opens a SQLite workspace file such as -// `.core/state/scroll-session-2026-03-30.duckdb` and removes it when the -// workspace is committed or discarded. -// // Usage example: `workspace, err := storeInstance.NewWorkspace("scroll-session-2026-03-30"); if err != nil { return }; defer workspace.Discard()` +// This creates `.core/state/scroll-session-2026-03-30.duckdb` by default and +// removes it when the workspace is committed or discarded. func (storeInstance *Store) NewWorkspace(name string) (*Workspace, error) { if err := storeInstance.ensureReady("store.NewWorkspace"); err != nil { return nil, err @@ -218,11 +214,9 @@ func workspaceNameFromPath(stateDirectory, databasePath string) string { return core.TrimSuffix(relativePath, ".duckdb") } -// RecoverOrphans(".core/state") returns orphaned workspaces such as -// `scroll-session-2026-03-30.duckdb` so callers can inspect Aggregate() and -// choose Commit() or Discard(). -// // Usage example: `orphans := storeInstance.RecoverOrphans(".core/state"); for _, orphanWorkspace := range orphans { fmt.Println(orphanWorkspace.Name(), orphanWorkspace.Aggregate()) }` +// This reopens leftover `.duckdb` files such as `scroll-session-2026-03-30` +// so callers can inspect `Aggregate()` and choose `Commit()` or `Discard()`. func (storeInstance *Store) RecoverOrphans(stateDirectory string) []*Workspace { if storeInstance == nil { return nil @@ -291,10 +285,9 @@ func (workspace *Workspace) Aggregate() map[string]any { return fields } -// Commit writes one completed workspace row to the journal and upserts the -// summary entry in `workspace:NAME`. -// // Usage example: `result := workspace.Commit(); if !result.OK { return }; fmt.Println(result.Value)` +// `Commit()` writes one completed workspace row to the journal, upserts the +// `workspace:NAME/summary` entry, and removes the workspace file. func (workspace *Workspace) Commit() core.Result { if err := workspace.ensureReady("store.Workspace.Commit"); err != nil { return core.Result{Value: err, OK: false} @@ -321,10 +314,9 @@ func (workspace *Workspace) Discard() { _ = workspace.closeAndRemoveFiles() } -// Query runs SQL against the workspace buffer and returns rows as -// `[]map[string]any` for ad-hoc inspection. -// // Usage example: `result := workspace.Query("SELECT entry_kind, COUNT(*) AS count FROM workspace_entries GROUP BY entry_kind")` +// `result.Value` contains `[]map[string]any`, which lets an agent inspect the +// current buffer state without defining extra result types. func (workspace *Workspace) Query(query string) core.Result { if err := workspace.ensureReady("store.Workspace.Query"); err != nil { return core.Result{Value: err, OK: false}