Files
Maven/internal/store/store.go
T
kami d52f60c54e maven: fix test mocks for CalendarEvents interface (verification)
- Add CalendarEvents method to recordingAPI in auth_test.go
- Add CalendarEvents method to fakeCore in handlers_test.go

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:20:16 +04:00

127 lines
4.1 KiB
Go

// Package store is maven's persistent state layer.
//
// The layer is append-only: a wrong fact is superseded, not overwritten.
// Current value for a key = the latest non-voided fact row.
//
// Schema lives in schema.sql and is applied idempotently on Open.
package store
import (
"context"
"database/sql"
_ "embed" // schema.sql
"errors"
"fmt"
"time"
_ "modernc.org/sqlite"
)
//go:embed schema.sql
var schemaSQL string
// FactKind — self | env | config. The loop only evaluates predicates against
// `self` and `config` rows; `env` feeds env predicates (calendar/weather/health).
type FactKind string
const (
KindSelf FactKind = "self"
KindEnv FactKind = "env"
KindConfig FactKind = "config"
)
// Fact — one observation. ts is valid-time (true-as-of), not insert-time.
type Fact struct {
ID int64
Ts time.Time
Kind FactKind
Key string
Value string // raw json if structured
Source string // tap:* | infer:* | poll:* | ambient | promote | feedback
Confidence float64
VoidsID sql.NullInt64
}
// Bucket — presence hysteresis state.
type Bucket string
const (
Present Bucket = "present"
Away Bucket = "away"
)
// Store is the persistent state layer. All writes are append-only; nothing
// here performs an UPDATE of a fact value (status-flips on reminders/nudges
// are the documented exceptions — they mutate small state-machine columns).
type Store struct {
db *sql.DB
enc *encState // nil ⇒ plaintext store (dev/CI); set ⇒ re-encrypt on Close
}
// openAt opens or creates the sqlite database at path and applies schema +
// migrations. This is the raw plaintext open used by both Open (plaintext
// store) and OpenEncrypted (the tmpfs working copy).
//
// Pragmas (WAL, NORMAL, FK on, busy_timeout) are set in schema.sql and re-applied
// per connection on open via the modernc driver DSN.
func openAt(ctx context.Context, path string) (*sql.DB, error) {
// `_pragma=busy_timeout(5000)` etc. embed cleanly; schema.sql sets them too.
dsn := fmt.Sprintf("file:%s?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)", path)
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, fmt.Errorf("open %s: %w", path, err)
}
// single writer expected; the daemon is the only process touching the db.
db.SetMaxOpenConns(1)
if _, err := db.ExecContext(ctx, schemaSQL); err != nil {
if closeErr := db.Close(); closeErr != nil {
return nil, fmt.Errorf("apply schema: %w (close: %v)", err, closeErr)
}
return nil, fmt.Errorf("apply schema: %w", err)
}
if err := migrate(ctx, db); err != nil {
if closeErr := db.Close(); closeErr != nil {
return nil, fmt.Errorf("migrate: %w (close: %v)", err, closeErr)
}
return nil, fmt.Errorf("migrate: %w", err)
}
return db, nil
}
// Open opens or creates a PLAINTEXT sqlite database at path. Used by tests and
// by any deployment that keeps the db unencrypted (CI, dev). Production goes
// through OpenEncrypted.
func Open(ctx context.Context, path string) (*Store, error) {
db, err := openAt(ctx, path)
if err != nil {
return nil, err
}
return &Store{db: db}, nil
}
// Close checkpoints, releases the database handle, and — for an encrypted
// store — re-encrypts the tmpfs working copy back to the on-disk ciphertext
// file atomically, then wipes the plaintext copy and zeroes the key.
func (s *Store) Close() error {
if s.enc == nil {
return s.db.Close()
}
return s.enc.closeAndSeal(s.db)
}
// DB exposes the underlying handle for internal read-only snapshots.
// Used by the loop to take a consistent read under a single transaction.
// Modules never receive this handle — core mediates.
func (s *Store) DB(ctx context.Context) (*sql.Tx, error) {
return s.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
}
var (
// ErrNoFact — no non-voided row exists for this key.
ErrNoFact = errors.New("store: no fact for key")
// ErrConfidence — a write attempted to use a confidence outside (0,1.0].
ErrConfidence = errors.New("store: confidence must be in (0.0, 1.0]")
// ErrVoidsMissing — a correction pointed at a nonexistent fact.
ErrVoidsMissing = errors.New("store: voids_id does not reference an existing fact")
)