-- maven core schema — append-only, three shapes.
-- a wrong fact is superseded, never overwritten. current value = latest non-voided row for a key.

PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA foreign_keys=ON;
PRAGMA busy_timeout=5000;

-- facts — substrate, all observations (self + env + config).
-- ts = valid-time (true-as-of), not insert-time.
-- source: tap:* | infer:* | poll:* | ambient | promote | feedback
-- confidence = 1.0 for taps only; <1 for inferred.
-- voids_id points at the fact this one cancels (correction).
CREATE TABLE IF NOT EXISTS facts (
  id           INTEGER PRIMARY KEY AUTOINCREMENT,
  ts           INTEGER NOT NULL,           -- unix epoch millis (valid-time)
  kind         TEXT    NOT NULL CHECK (kind IN ('self','env','config')),
  key          TEXT    NOT NULL,
  value        TEXT    NOT NULL,            -- json if structured
  source       TEXT    NOT NULL,
  confidence   REAL    NOT NULL DEFAULT 1.0 CHECK (confidence > 0.0 AND confidence <= 1.0),
  voids_id     INTEGER REFERENCES facts(id)
);
CREATE INDEX IF NOT EXISTS idx_facts_key_ts ON facts (key, ts DESC);
CREATE INDEX IF NOT EXISTS idx_facts_voids  ON facts (voids_id);

-- reminders — user intent, fires once or recurring (if cron set).
-- relative→absolute happens at capture ("in 4h" → store now+4h, never the string).
CREATE TABLE IF NOT EXISTS reminders (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  created_ts   INTEGER NOT NULL,            -- epoch millis
  fire_ts     INTEGER NOT NULL,             -- epoch millis
  payload     TEXT    NOT NULL,             -- json
  status      TEXT    NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','fired','cancelled'))
);
CREATE INDEX IF NOT EXISTS idx_reminders_fire   ON reminders (fire_ts) WHERE status = 'pending';
CREATE INDEX IF NOT EXISTS idx_reminders_status ON reminders (status);

-- nudges — every proactive send + outcome. this table IS the restraint memory.
CREATE TABLE IF NOT EXISTS nudges (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  ts          INTEGER NOT NULL,             -- sent ts, epoch millis
  rule        TEXT    NOT NULL,
  channel     TEXT    NOT NULL,
  message     TEXT    NOT NULL,
  outcome     TEXT    NOT NULL DEFAULT 'pending' CHECK (outcome IN ('pending','acted','snoozed','ignored')),
  outcome_ts  INTEGER
);
CREATE INDEX IF NOT EXISTS idx_nudges_rule_ts ON nudges (rule, ts DESC);
CREATE INDEX IF NOT EXISTS idx_nudges_outcome ON nudges (outcome);

-- notes — semantic recall/preference store. no predicate reads these (that's
-- what facts are for); query-answering does brute-force cosine over embedding.
-- embedding is a little-endian float32 BLOB. no index — personal-scale scan.
CREATE TABLE IF NOT EXISTS notes (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  ts          INTEGER NOT NULL,             -- epoch millis
  text        TEXT    NOT NULL,
  embedding   BLOB    NOT NULL,             -- little-endian float32[]
  source      TEXT    NOT NULL
);

-- tools — the act allowlist maven executes. proposed rows are scaffolds maven
-- drafts when she hits an act she can't run; enabling (filling cmd + flipping
-- status) is a human act through an authed surface, NEVER the voice path. a
-- proposed row drives nothing — the executor only runs status='enabled' rows.
CREATE TABLE IF NOT EXISTS tools (
  name        TEXT    PRIMARY KEY,        -- the spoken verb ("restart")
  cmd         TEXT    NOT NULL DEFAULT '[]', -- json argv prefix; args appended at run
  destructive INTEGER NOT NULL DEFAULT 0,  -- 1 ⇒ needs a confirm turn before it runs
  status      TEXT    NOT NULL DEFAULT 'proposed' CHECK (status IN ('proposed','enabled')),
  utterance   TEXT    NOT NULL DEFAULT '', -- the utterance that scaffolded a proposal (provenance)
  created_ts  INTEGER NOT NULL,
  updated_ts  INTEGER NOT NULL
);

-- presence_state — the only stateful bit of presence (a pure function otherwise).
-- hysteresis bucket; updated each tick after resolve().
CREATE TABLE IF NOT EXISTS presence_state (
  id           INTEGER PRIMARY KEY CHECK (id = 1),  -- singleton row
  last_bucket  TEXT    NOT NULL CHECK (last_bucket IN ('present','away')),
  last_score   REAL    NOT NULL,
  updated_ts   INTEGER NOT NULL
);