// 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 } // Open opens or creates the sqlite database at path and applies the schema. // 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 Open(ctx context.Context, path string) (*Store, 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) } return &Store{db: db}, nil } // Close releases the database handle. func (s *Store) Close() error { return s.db.Close() } // 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") )