sqlite

package
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 46 Imported by: 0

Documentation

Overview

Package sqlite is the SQLite-backed implementation of the wayneblacktea storage interfaces, intended for friend-grade self-hosting (one binary + one .db file, no Postgres server).

Schema evolution (as of E1, 2026-07-18): Open() applies the embedded migrations/sqlite/*.sql migration set via golang-migrate at connection time — see migrate.go. schema.sql is retired as the runtime authority and kept only as a historical test fixture (see its own header comment); the canonical target schema snapshot is testdata/schema_golden.sql.

Driver: modernc.org/sqlite (pure Go, no CGo) so cross-compilation to Linux/macOS/arm64 stays painless.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotImplemented = errors.New("sqlite store: not yet implemented in this build")

ErrNotImplemented is returned by stub stores in this package whose backend implementation is still pending. Test for it with errors.Is.

View Source
var ErrSchemaNotCurrent = errors.New("sqlite: database schema is not current (readonly connections never migrate)")

ErrSchemaNotCurrent is returned by OpenReadOnly when the database at path does not have a schema_migrations row matching latestSQLiteSchemaVersion with dirty=0 — including a database with no schema_migrations table at all (a pre-migration-runner snapshot, or a brand-new/empty file). OpenReadOnly never runs migrations (a write operation on what the caller has explicitly asked to be a read-only connection — see dsnReadOnly), so a stale or unreadable schema is a hard error rather than a silent partial-schema open.

Functions

func NewAcceptAdapter

func NewAcceptAdapter(id uuid.UUID, deps AcceptDeps) proposal.AcceptAdapter

NewAcceptAdapter constructs a fresh sqliteAcceptAdapter for one proposal.AcceptOrchestration(ctx, id, adapter) call.

Types

type AcceptDeps

type AcceptDeps struct {
	Proposal  *ProposalStore
	GTD       *GTDStore
	Learning  *LearningStore
	Decision  *DecisionStore
	Knowledge *KnowledgeStore
}

AcceptDeps groups the concrete SQLite store handles sqliteAcceptAdapter needs. Threaded in from a storage.ServerStores by internal/storage's AcceptSeam — see proposal.PgAcceptDeps's doc comment for why AcceptSeam itself lives in internal/storage rather than internal/proposal or here.

type ArchStore

type ArchStore struct {
	// contains filtered or unexported fields
}

ArchStore is the SQLite-backed implementation of arch.StoreIface.

func NewArchStore

func NewArchStore(d *DB) *ArchStore

NewArchStore wraps an open DB into an ArchStore.

func (*ArchStore) GetSnapshot

func (s *ArchStore) GetSnapshot(ctx context.Context, slug string) (*arch.Snapshot, error)

GetSnapshot returns the snapshot for the given slug.

func (*ArchStore) UpsertSnapshot

func (s *ArchStore) UpsertSnapshot(ctx context.Context, p arch.UpsertParams) (*arch.Snapshot, error)

UpsertSnapshot inserts or updates the architecture snapshot for the given slug.

type AtomStore

type AtomStore struct {
	// contains filtered or unexported fields
}

AtomStore is the SQLite-backed implementation of atom.StoreIface.

func NewAtomStore

func NewAtomStore(d *DB) *AtomStore

NewAtomStore wraps an open DB into an AtomStore.

func (*AtomStore) AddAtom

func (s *AtomStore) AddAtom(ctx context.Context, p atom.AddAtomParams) (*atom.Atom, error)

AddAtom inserts a new atom and returns the persisted record.

func (s *AtomStore) AddLink(ctx context.Context, p atom.AddLinkParams) error

AddLink inserts a directed link. INSERT OR IGNORE makes it idempotent.

func (*AtomStore) CountByDigestStatus

func (s *AtomStore) CountByDigestStatus(ctx context.Context, workspaceID *uuid.UUID, status string) (int64, error)

CountByDigestStatus counts atoms with the given digest_status, optionally scoped to a workspace.

func (*AtomStore) CountTotal

func (s *AtomStore) CountTotal(ctx context.Context, workspaceID *uuid.UUID) (int64, error)

CountTotal returns the total number of atoms scoped to the given workspace.

func (*AtomStore) ListByDigestStatus

func (s *AtomStore) ListByDigestStatus(ctx context.Context, workspaceID *uuid.UUID, status string, limit int) ([]atom.Atom, error)

ListByDigestStatus returns up to limit atoms with the given digest_status, ordered by created_at ASC. Mirrors the CountByDigestStatus query shape.

func (*AtomStore) ListByParent

func (s *AtomStore) ListByParent(ctx context.Context, parentTable string, parentID uuid.UUID) ([]atom.Atom, error)

ListByParent returns all atoms for a given parent table + id.

func (*AtomStore) PruneAtoms

func (s *AtomStore) PruneAtoms(ctx context.Context, cutoff time.Time) (int64, error)

PruneAtoms hard-deletes memory_atoms rows older than cutoff. Called by the daily decay pruner to enforce the 90-day TTL. Link rows referencing pruned atoms are deleted first inside a transaction (no FK cascade per project red-line #9; referential integrity enforced in code).

func (*AtomStore) Search

func (s *AtomStore) Search(ctx context.Context, workspaceID *uuid.UUID, query string, limit int) ([]atom.Atom, error)

Search returns atoms whose content, keywords, or tags match query (LIKE).

func (*AtomStore) SetDigestStatus

func (s *AtomStore) SetDigestStatus(ctx context.Context, id uuid.UUID, status string, errMsg string) error

SetDigestStatus updates the digest_status and optional error_msg for a single atom. status is validated against the five-value enum (internal/atom/digest_status.go) before the UPDATE is sent — mirrors the Postgres Store's validation so both backends reject the same invalid values identically (backend-security-design.md §2, §6.3 PG/SQLite parity).

func (*AtomStore) Traverse

func (s *AtomStore) Traverse(ctx context.Context, startAtomID uuid.UUID, depth int) (*atom.TraverseResult, error)

Traverse performs iterative BFS from startAtomID up to min(depth, 5) hops.

type BehaviorRuleStore

type BehaviorRuleStore struct {
	// contains filtered or unexported fields
}

BehaviorRuleStore is the SQLite-backed implementation of behaviorrule.StoreIface.

func NewBehaviorRuleStore

func NewBehaviorRuleStore(d *DB) *BehaviorRuleStore

NewBehaviorRuleStore wraps an open DB into a BehaviorRuleStore.

func (*BehaviorRuleStore) ApplyOutcome

func (s *BehaviorRuleStore) ApplyOutcome(ctx context.Context, id uuid.UUID, outcome string) (*behaviorrule.BehaviorRule, error)

ApplyOutcome applies the confidence formula atomically in a single UPDATE. SQLite uses CASE expressions to implement the atomic update without a read-then-write race. SECURITY: workspace-scoped — matches List's scoping and the Postgres Store, so a store scoped to workspace A cannot apply an outcome to a rule owned by workspace B.

func (*BehaviorRuleStore) Deprecate

Deprecate sets the rule's status to 'deprecated'. Idempotent. SECURITY: workspace-scoped — matches List's scoping and the Postgres Store.

func (*BehaviorRuleStore) List

List returns behavior rules matching the filter, ordered by created_at DESC.

func (*BehaviorRuleStore) Propose

Propose inserts a new behavior rule with status='proposed' and returns the persisted record.

func (*BehaviorRuleStore) PruneOlderThan

func (s *BehaviorRuleStore) PruneOlderThan(ctx context.Context, cutoff time.Time) (int64, error)

PruneOlderThan hard-deletes behavior_rules rows where status IN ('rejected','deprecated') AND created_at < cutoff. Active and proposed rows are NEVER deleted regardless of age.

type CognitiveJobsStore

type CognitiveJobsStore struct {
	// contains filtered or unexported fields
}

CognitiveJobsStore is the SQLite-backed implementation of scheduler.CognitiveSQLiteStore — the narrow query surface backing the 4 scheduler cognitive jobs (stuck_task_detection, decision_outcome_review, knowledge_to_skill_candidate, proposal_cleanup) whose Postgres implementation is raw SQL against the scheduler's own disciplinePool (internal/scheduler/cognitive_jobs.go). Kept in its own file — separate from GTDStore/DecisionStore/KnowledgeStore/ProposalStore — because these 4 methods are scheduler-local plumbing, NOT part of gtd/decision/knowledge/proposal.StoreIface (backend-security-design.md domain-ownership rule). GTD decision G4 (6ea0b014): jobs whose proposals are user-observable get SQLite parity; jobs that only prune disk-growth-only observability tables stay Postgres-only (see internal/scheduler/scheduler.go's pgOnlyJobs capability contract — that's a DIFFERENT, deliberately-not-implemented set from the 4 methods here).

func NewCognitiveJobsStore

func NewCognitiveJobsStore(d *DB) *CognitiveJobsStore

NewCognitiveJobsStore wraps an open DB into a CognitiveJobsStore.

func (*CognitiveJobsStore) DecisionsPendingOutcomeReview

func (s *CognitiveJobsStore) DecisionsPendingOutcomeReview(
	ctx context.Context, olderThan time.Duration, limit int,
) ([]db.Decision, error)

DecisionsPendingOutcomeReview returns decisions created before the olderThan cutoff that have no recorded outcome AND no existing pending scheduler:decision_outcome_review proposal (dedup — mirrors the Postgres NOT EXISTS guard added by the 2026-07-19 incident fix, see decisionOutcomeReviewDailyCap's doc comment in cognitive_jobs.go). Ordered oldest-first and capped at limit rows so consecutive daily runs drain a backlog forward instead of reprocessing the same head-of-queue rows. pending_proposals.payload is stored as TEXT JSON in SQLite (unlike Postgres's JSONB), so the dedup match uses json_extract instead of the Postgres `->>'source_entity_id'` operator — modernc.org/sqlite ships JSON1 built in.

func (*CognitiveJobsStore) ExpireStaleScheduledProposals

func (s *CognitiveJobsStore) ExpireStaleScheduledProposals(
	ctx context.Context, olderThan time.Duration, reason string,
) (int64, error)

ExpireStaleScheduledProposals marks scheduler-originated pending proposals older than olderThan as rejected with reason, returning the number of rows affected. This is a SQLite-native reimplementation of the retention arithmetic — NOT a call-through to any Postgres code path — per GTD decision G4 (6ea0b014): SQLite has no `NOW() - INTERVAL` syntax, so the cutoff is computed in Go and bound as a parameter instead of the Postgres interval literal. Deliberately NOT workspace-scoped, matching the Postgres job's actual behaviour (it expires stale scheduler proposals across every workspace, not just one — see cognitive_jobs.go runProposalCleanup's PG branch). The proposed_by LIKE 'scheduler:%' guard is the critical safety boundary and MUST NOT be widened to touch user-submitted proposals (mirrors the Postgres comment).

func (*CognitiveJobsStore) HighRecallKnowledgeItems

func (s *CognitiveJobsStore) HighRecallKnowledgeItems(ctx context.Context, minRecallCount int) ([]db.KnowledgeItem, error)

HighRecallKnowledgeItems returns non-archived knowledge items whose recall_count exceeds minRecallCount, scoped to the configured workspace, ordered by recall_count descending. Mirrors the Postgres query in cognitive_jobs.go's pgHighRecallKnowledgeItems (job 4), including its dedup guard: an item already carrying a pending scheduler:knowledge_to_skill proposal (matched via json_extract, same SQLite/JSONB dialect split documented on StuckTasks above) is excluded. Reuses knowledgeSelectCols/scanKnowledgeItem (knowledge.go).

func (*CognitiveJobsStore) StuckTasks

func (s *CognitiveJobsStore) StuckTasks(ctx context.Context, olderThan time.Duration) ([]db.Task, error)

StuckTasks returns in_progress tasks whose updated_at is older than olderThan, scoped to the configured workspace and ordered oldest-first. Mirrors the Postgres query in cognitive_jobs.go's pgStuckTasks (job 2), including its dedup guard: a task already carrying a pending scheduler:stuck_task proposal (matched via json_extract(payload,'$.source_entity_id'), the SQLite twin of Postgres's payload->>'source_entity_id' — pending_proposals.payload is TEXT JSON in SQLite, not JSONB, so `->>` isn't available; modernc.org/sqlite ships JSON1 built in) is excluded so a follow-up run doesn't re-propose it. Reuses tasksSelectCols/scanTask (gtd.go) so the returned db.Task rows are identical in shape to every other SQLite task read.

type DB

type DB struct {
	// contains filtered or unexported fields
}

DB is the package-internal connection wrapper. It holds the *sql.DB plus the optional workspace UUID (echoing the Postgres stores' Init-time scoping). Stores share this and add their own typed methods on top.

func Open

func Open(ctx context.Context, dsn, workspaceID string) (*DB, error)

Open creates a new DB by opening dsn (e.g. "file:wbt.db" or ":memory:") and bringing its schema up to date via the embedded golang-migrate migration set (migrations/sqlite/); see runMigrations for the adoption-vs-replay logic. workspaceID may be empty. schema.sql is no longer the runtime authority — see its header comment.

func OpenReadOnly

func OpenReadOnly(ctx context.Context, path, workspaceID string) (*DB, error)

OpenReadOnly opens path strictly read-only — no migrations, no PRAGMA writes to the database content, no chmod — and verifies its schema_migrations row matches latestSQLiteSchemaVersion with dirty=0 before returning.

It does NOT guarantee zero filesystem writes: opening a WAL-mode database with mode=ro still causes SQLite to create "-wal"/"-shm" side-car files next to it if they are not already present (empirically verified against modernc.org/sqlite v1.50.0 — the "-wal" side-car is created 0 bytes and stays 0 bytes since a read-only connection never appends WAL frames; the "-shm" side-car is the shared-memory index SQLite always mmaps for a WAL-mode db, regardless of read/write mode). Both side-cars are created with the SAME permission bits as the main db file (verified: chmod-ing the main file to 0400 before OpenReadOnly produces 0400 side-cars, not a process-umask default) — see TestOpenReadOnly_SideCarPermissionsMatchMainFile in readonly_test.go for the pinned proof. They are NOT removed by DB.Close(). This is an accepted trade-off, not a bug: it means a hook process running against a read-only-intended directory can leave two small files behind (polluting e.g. `git status` on a cloned repo), but it cannot leak data (the "-wal" side-car carries no WAL frames from this connection) and it cannot widen the file's exposure (permissions always match the main file, never broader).

Intended for hook binaries (wbt context session-start) that must read an existing, possibly-untrusted-directory SQLite file without ever mutating its CONTENT — see backend-security-design.md §2.2/§5.1/§5.3 and the A5a dispatch's M-1 threat model (a repo-shipped .db file must never be a write vector, and a hook process must never be the thing that migrates a shared DB out from under a longer-lived process).

path MUST be a plain filesystem path, not a "file:" URI or any DSN with a query string or fragment — OpenReadOnly builds the URI form itself (see dsnReadOnly) so the mode=ro query parameter can never be stripped, duplicated, or overridden by caller-controlled input.

Returns the package's own *DB (not *sql.DB) so callers can pass it straight into every NewXStore constructor in this package unchanged — those all take *DB (see e.g. NewGTDStore, NewDecisionStore).

func (*DB) BeginTx

func (d *DB) BeginTx(ctx context.Context) (*sql.Tx, error)

BeginTx starts a new database transaction scoped to ctx. The caller is responsible for calling Commit or Rollback on the returned *sql.Tx. Exported so multi-store cross-domain operations (e.g. the confirm_proposal accept path) can wrap several store writes in one atomic SQLite transaction.

func (*DB) Close

func (d *DB) Close() error

Close releases the underlying connection and the modeof mode-reference descriptor (see DB.modeReference) kept open for the DSN's entire life. Safe on a nil *DB or nil modeReference (closeModeReference is a no-op for nil, matching the :memory: / non-file DSN case).

Idempotent: *sql.DB.Close() is documented idempotent (returns nil on every call after the first), but the underlying *os.File modeReference is not — a second os.File.Close() returns an already-closed error. d.modeReference is nilled out after the first Close() attempt so a repeat call's closeModeReference(nil) short-circuits to nil, matching *sql.DB's own idempotent contract instead of surfacing a spurious error on a resource that's already gone.

func (*DB) ExecContext

func (d *DB) ExecContext(ctx context.Context, query string, args ...any) error

ExecContext executes a query on the underlying connection. Exported so integration tests in sibling packages can insert fixture rows (e.g. parent tasks / sessions for cascade-cleanup tests) without depending on production write paths.

func (*DB) QueryRowContext

func (d *DB) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row

QueryRowContext is a thin wrapper exposed for sibling-package tests to assert post-condition state (e.g. that a referential cleanup performed by a service-layer DeleteTask actually NULL'd a column / removed a row). Production code paths inside the package use s.db.conn directly.

func (*DB) SqlConn

func (d *DB) SqlConn() *sql.DB

SqlConn returns the underlying *sql.DB. Callers should prefer the typed store methods; this accessor exists for domain stores (like completioncandidate) that need sql.Rows-based list queries not covered by ExecContext/QueryRowContext. The *sql.DB is the same connection pool shared by all stores — no extra connections are opened. SQLite max-conns is 1 (set in Open), so concurrent writers are serialised automatically.

func (*DB) WorkspaceID

func (d *DB) WorkspaceID() string

WorkspaceID returns the configured workspace UUID string, or "" when operating in legacy unscoped mode.

type DecisionStore

type DecisionStore struct {
	// contains filtered or unexported fields
}

DecisionStore is the SQLite-backed implementation of decision.StoreIface.

func NewDecisionStore

func NewDecisionStore(d *DB) *DecisionStore

NewDecisionStore wraps an open DB into a DecisionStore.

func (*DecisionStore) All

func (s *DecisionStore) All(ctx context.Context, limit int32) ([]db.Decision, error)

All returns the most recent decisions across all repos and projects.

func (*DecisionStore) ByProject

func (s *DecisionStore) ByProject(ctx context.Context, projectID uuid.UUID, limit int32) ([]db.Decision, error)

ByProject returns the most recent decisions for a given project ID.

func (*DecisionStore) ByRepo

func (s *DecisionStore) ByRepo(ctx context.Context, repoName string, limit int32) ([]db.Decision, error)

ByRepo returns the most recent decisions for a given repo name.

func (*DecisionStore) ByTask

func (s *DecisionStore) ByTask(ctx context.Context, taskID uuid.UUID, limit int32) ([]db.Decision, error)

ByTask returns the most recent decisions for a given task ID. Migration 000048 added task_id to the decisions table. SECURITY: workspace-scoped.

func (*DecisionStore) ImportDecision

func (s *DecisionStore) ImportDecision(ctx context.Context, d db.Decision) error

ImportDecision inserts a decision row using d's own id/created_at instead of generating fresh ones, so decisions that reference a task_id/project_id stay linked to the same rows imported by ImportProject/ImportTask. Used by cmd/qa-seed. Embedding columns are intentionally left NULL (all nullable) — pgvector recall is out of the QA-seed v1 scope; only CRUD-shaped fields used by the frontend are copied. Fails (no upsert) on a duplicate id — callers MUST import into a fresh database. d.Source is validated before write, same as Log/LogTx — the source-of-truth Postgres row is expected to already be valid, but this guard doesn't delegate that assumption to the DB CHECK constraint (backend-security-design.md §5.2; security review round 2, m-1).

func (*DecisionStore) List

List returns decisions filtered by p (project XOR repo, plus IncludeAuto). Workspace scoping comes from s.db.workspaceArg() (bound at Open time), never from p — mirrors the PG Store.List behaviour. Source is filtered BEFORE ORDER/LIMIT so the limit isn't consumed by rows that get excluded.

func (*DecisionStore) Log

Log records a new architectural decision. task_id (migration 000048) is stored as nullable TEXT UUID. p.Source is validated before write — an invalid Source writes zero rows. actor_session_id/confirmed_by_human (migration 000076) round-trip whatever the caller set on p — see LogParams's doc comments; this contract layer's own callers all currently leave them at zero value.

func (*DecisionStore) LogTx

func (s *DecisionStore) LogTx(ctx context.Context, tx *sql.Tx, p decision.LogParams) (uuid.UUID, error)

LogTx is the transactional counterpart of Log. It inserts the decision row inside the supplied *sql.Tx so the caller (confirm_proposal accept path for type='decision') can atomically commit the materialised decision and the proposal-resolve in a single transaction. Returns the freshly-generated ID on success — callers that want the full row can SELECT it after Commit. p.Source is validated before write — an invalid Source writes zero rows.

func (*DecisionStore) SearchByCosine

func (s *DecisionStore) SearchByCosine(_ context.Context, _ []float32, _ int) ([]db.Decision, error)

SearchByCosine always returns decision.ErrCosineUnsupported: the SQLite decisions table has no embedding column (migration 000020 added it, 000026's FK-drop table rebuild never carried it into decisions_new, and it has never been restored — see migrations/HISTORICAL_EXCEPTIONS.md and migrations/sqlite/000064_embedding_provider_marker.up.sql's comment). This method issues no SQL — querying a column that structurally doesn't exist would only ever fail with "no such column: embedding", so the failure is reported directly as a capability error instead of via a doomed round trip to the database. Kept on DecisionStore (rather than removed) because decision.StoreIface requires it — callers use errors.Is to detect the capability gap and degrade deliberately (e.g. skip semantic recall for decisions) rather than treating a nil/empty result as "no data matched".

SECURITY: no query is issued, so there is nothing to scope by workspace_id here — this is intentionally a no-op, not a search.

type DisciplineEventM8Store

type DisciplineEventM8Store struct {
	// contains filtered or unexported fields
}

DisciplineEventM8Store is the SQLite-backed implementation of watchdog.DisciplineEventStoreIface for the discipline_events_m8 table.

func NewDisciplineEventM8Store

func NewDisciplineEventM8Store(d *DB) *DisciplineEventM8Store

NewDisciplineEventM8Store returns a DisciplineEventM8Store backed by an open SQLite DB.

func (*DisciplineEventM8Store) Insert

Insert records a new discipline_events_m8 row with an application-generated UUID (SQLite has no gen_random_uuid()).

func (*DisciplineEventM8Store) ListUnresolved

func (s *DisciplineEventM8Store) ListUnresolved(ctx context.Context, wsID *uuid.UUID) ([]watchdog.DisciplineEvent, error)

ListUnresolved returns all open events (resolved_at IS NULL), optionally scoped to the given workspace UUID.

func (*DisciplineEventM8Store) MarkResolved

func (s *DisciplineEventM8Store) MarkResolved(ctx context.Context, eventID uuid.UUID) error

MarkResolved sets resolved_at = NOW() for the given event.

func (*DisciplineEventM8Store) PruneOlderThan

func (s *DisciplineEventM8Store) PruneOlderThan(ctx context.Context, cutoff time.Time) (int64, error)

PruneOlderThan hard-deletes rows with created_at < cutoff.

type DisciplineStore

type DisciplineStore struct {
	// contains filtered or unexported fields
}

DisciplineStore is the SQLite-backed implementation of discipline.Store.

func NewDisciplineStore

func NewDisciplineStore(d *DB) *DisciplineStore

NewDisciplineStore wraps an open DB into a DisciplineStore.

func (*DisciplineStore) Insert

Insert records a single discipline event. WorkspaceID falls back to the DB's configured workspace when the param value is nil.

func (*DisciplineStore) RecentDecisionTimes

func (s *DisciplineStore) RecentDecisionTimes(ctx context.Context, sessionID string, since time.Time) ([]time.Time, error)

RecentDecisionTimes returns observed_at timestamps of log_decision / confirm_plan events for the given session at or after `since`, newest first.

Scoping mirrors RecentMutating: scoped DB sees only its own workspace_id; unscoped DB sees only NULL workspace_id rows. The two are disjoint.

func (*DisciplineStore) RecentMutating

func (s *DisciplineStore) RecentMutating(ctx context.Context, since time.Time, limit int) ([]discipline.Event, error)

RecentMutating returns mutating events from the configured workspace observed at or after `since`, newest first, capped at limit.

Scoping: when the DB has a workspaceID set, only rows with that exact workspace_id are returned. When the DB has no workspaceID (legacy single-tenant mode), only rows whose workspace_id IS NULL are returned. The two are disjoint — there is no fallback that lets an unscoped store see scoped rows or vice-versa. Mirrors PgStore.RecentMutating.

type GTDStore

type GTDStore struct {
	// contains filtered or unexported fields
}

GTDStore is the SQLite-backed implementation of gtd.StoreIface.

func NewGTDStore

func NewGTDStore(d *DB) *GTDStore

NewGTDStore wraps an open DB into a GTDStore.

func (*GTDStore) ActiveGoals

func (s *GTDStore) ActiveGoals(ctx context.Context) ([]db.Goal, error)

ActiveGoals returns all active goals ordered by due_date ascending NULLS last.

func (*GTDStore) AddChecklistItem

func (s *GTDStore) AddChecklistItem(
	ctx context.Context, taskID uuid.UUID, workspaceID uuid.UUID, item gtd.ChecklistItem,
) ([]gtd.ChecklistItem, error)

AddChecklistItem appends a new ChecklistItem (server-generated ID) to the task's checklist and returns the full updated slice. Returns gtd.ErrNotFound when no task matches taskID + workspaceID.

The load+save pair runs inside a BEGIN IMMEDIATE transaction to prevent lost updates when concurrent callers modify the same task's checklist.

func (*GTDStore) BatchCompleteTasksByPRMatch

func (s *GTDStore) BatchCompleteTasksByPRMatch(ctx context.Context, matches []gtd.Match) (map[uuid.UUID]bool, error)

BatchCompleteTasksByPRMatch implements gtd.StoreIface.BatchCompleteTasksByPRMatch for the SQLite backend. Runs inside a single *sql.Tx so a partial auto-close cannot leave half the matches done. See internal/gtd/iface.go for contract.

func (*GTDStore) BeginTask

func (s *GTDStore) BeginTask(ctx context.Context, id uuid.UUID) (*db.Task, error)

BeginTask atomically sets a task to in_progress and records a work_session_started activity log entry.

Idempotency: if the task is already in_progress, the task row is returned as-is without writing a duplicate activity_log row. Returns gtd.ErrNotFound when no task matches id inside the configured workspace.

func (*GTDStore) CompleteTask

func (s *GTDStore) CompleteTask(ctx context.Context, id uuid.UUID, artifact *string) (*db.Task, error)

CompleteTask marks a task completed and records the optional artifact URL. CompleteTask marks a task completed. artifact is presence-aware (Ω4, 2026-08-20-mcp-surface-spec.md): nil preserves whatever is already stored (COALESCE), matching the Postgres-side fix and upsert_project_arch's established summary/file_map convention. Without COALESCE here, re-completing a reopened task without re-supplying artifact silently wiped an already-recorded PR/commit link.

func (*GTDStore) CreateGoal

func (s *GTDStore) CreateGoal(ctx context.Context, p gtd.CreateGoalParams) (*db.Goal, error)

CreateGoal inserts a new goal.

func (*GTDStore) CreateGoalTx

func (s *GTDStore) CreateGoalTx(ctx context.Context, tx *sql.Tx, p gtd.CreateGoalParams) (uuid.UUID, error)

CreateGoalTx inserts a new goal within the provided *sql.Tx. It is the transactional counterpart of CreateGoal and is used by the confirm_proposal accept path for atomic cross-store writes.

func (*GTDStore) CreateProject

func (s *GTDStore) CreateProject(ctx context.Context, p gtd.CreateProjectParams) (*db.Project, error)

CreateProject inserts a new project, generating a UUID and returning the row. repo_name is persisted when non-empty; empty string stores NULL (parity with migration 000037 which added the nullable TEXT column).

func (*GTDStore) CreateProjectTx

func (s *GTDStore) CreateProjectTx(ctx context.Context, tx *sql.Tx, p gtd.CreateProjectParams) (uuid.UUID, error)

CreateProjectTx inserts a new project within the provided *sql.Tx. It is the transactional counterpart of CreateProject and is used by the confirm_proposal accept path for atomic cross-store writes.

repo_name validation was previously missing here even though the sibling non-Tx CreateProject (above) validates it and the Postgres equivalent (internal/gtd/store.go's Store.CreateProject, reused unmodified via WithTx(tx)) validates it too — a SQLite-only backend asymmetry in the confirm_proposal materialisation path (sprint 8-7 gap G).

func (*GTDStore) CreateTask

func (s *GTDStore) CreateTask(ctx context.Context, p gtd.CreateTaskParams) (*db.Task, error)

CreateTask inserts a new task with all Phase A/B fields supported. Hand-rolled INSERT (instead of sqlc CreateTask) so that branch_name, pr_url, and commit_shas columns added in migration 000047 are included without requiring a sqlc regeneration run.

func (*GTDStore) CreateTaskTx

func (s *GTDStore) CreateTaskTx(ctx context.Context, tx *sql.Tx, p gtd.CreateTaskParams) (uuid.UUID, error)

CreateTaskTx inserts a new task within the provided *sql.Tx. It is the transactional counterpart of CreateTask — mirroring CreateGoalTx / CreateProjectTx / LearningStore.CreateConceptTx — and is used by confirm_plan's SQLite atomic path so phase tasks and decisions commit or roll back together in one transaction. Returns the freshly-generated ID only (not the full row), matching the CreateGoalTx/CreateProjectTx return shape: callers that already know the input fields (confirm_plan does — it just supplied them) don't need a post-commit re-read to get them back.

func (*GTDStore) DB

func (s *GTDStore) DB() *DB

DB returns the underlying *DB handle. Exported so cross-store transactional callers (e.g. confirm_plan's SQLite atomic path, which needs to open one *sql.Tx and pass it to both CreateTaskTx here and DecisionStore.LogTx) can BeginTx once instead of each store opening its own — SQLite is single-writer, so two concurrently open transactions on the same underlying connection would deadlock.

func (*GTDStore) DeleteChecklistItem

func (s *GTDStore) DeleteChecklistItem(ctx context.Context, taskID uuid.UUID, workspaceID uuid.UUID, itemID uuid.UUID) error

DeleteChecklistItem removes the item identified by itemID from the task's checklist. Returns gtd.ErrNotFound when task or item is not found.

The load+save pair runs inside a BEGIN IMMEDIATE transaction to prevent lost updates when concurrent callers modify the same task's checklist.

func (*GTDStore) DeleteTask

func (s *GTDStore) DeleteTask(ctx context.Context, id uuid.UUID) error

DeleteTask permanently removes a task by ID and replicates the cascade behaviour previously enforced by foreign keys (red line #9; see migration 000026):

  • work_session_tasks rows referencing the deleted task are removed (was ON DELETE CASCADE)
  • work_sessions.current_task_id pointing at the deleted task is set NULL (was ON DELETE SET NULL)

All statements run inside a single SQLite transaction so a partial state is impossible. Tx pattern matches WorkSessionStore.Create (manual Begin + defer Rollback + Commit). Workspace authorisation is enforced by an explicit pre-check inside the tx BEFORE any cleanup runs: if the task does not exist in the configured workspace the call is a silent no-op (matching the pre-fix behaviour where a workspace-mismatched DELETE simply affected 0 rows on the parent table). The pre-check ensures cleanup never touches another workspace's join rows or work_sessions; the parent DELETE's workspace filter is now redundant defence-in-depth. See gtd.DeleteTaskOrchestration (internal/gtd/deletetask_orchestration.go) for the shared control flow this delegates to.

func (*GTDStore) GetProjectByID

func (s *GTDStore) GetProjectByID(ctx context.Context, id uuid.UUID) (*db.Project, error)

GetProjectByID returns a single project by UUID, regardless of status.

func (*GTDStore) GetTaskByID

func (s *GTDStore) GetTaskByID(ctx context.Context, id uuid.UUID) (*db.Task, error)

GetTaskByID returns a single task by UUID, scoped to the configured workspace. Returns ErrNotFound when no matching row exists. Satisfies gtd.StoreIface.

func (*GTDStore) ImportGoal

func (s *GTDStore) ImportGoal(ctx context.Context, g db.Goal) error

ImportGoal inserts a goal row using g's own id/created_at/updated_at/status instead of generating fresh ones. See ImportProject doc comment for the full rationale (cmd/qa-seed fidelity import).

func (*GTDStore) ImportProject

func (s *GTDStore) ImportProject(ctx context.Context, p db.Project) error

ImportProject inserts a project row using p's own id/created_at/updated_at instead of generating fresh ones, so cross-table references (goal_id, task.project_id) survive a Postgres-to-SQLite copy verbatim. Used by cmd/qa-seed to replicate production data into a disposable local SQLite file for integration-qa. Fails (no upsert) on a duplicate id — callers MUST import into a fresh database, never re-import into one that already has the row.

func (*GTDStore) ImportTask

func (s *GTDStore) ImportTask(ctx context.Context, t db.Task) error

ImportTask inserts a task row using t's own id/created_at/updated_at/status (plus checklist/vision_item_id, which CreateTask does not set) instead of generating fresh ones. See ImportProject doc comment for the full rationale (cmd/qa-seed fidelity import).

func (*GTDStore) LatestActionAt

func (s *GTDStore) LatestActionAt(ctx context.Context, action string) (*time.Time, error)

LatestActionAt returns the created_at of the most-recent activity_log row matching the given action, workspace-scoped, or nil if none found. SQLite parity with gtd.Store.LatestActionAt.

func (*GTDStore) LatestActivityAt

func (s *GTDStore) LatestActivityAt(ctx context.Context) (*time.Time, error)

LatestActivityAt returns the created_at of the most-recent activity_log row, workspace-scoped, or nil if the table is empty. SQLite parity with gtd.Store.LatestActivityAt.

func (*GTDStore) ListActiveProjects

func (s *GTDStore) ListActiveProjects(ctx context.Context) ([]db.Project, error)

ListActiveProjects returns all active projects in the configured workspace.

func (*GTDStore) ListActivityLogsSince

func (s *GTDStore) ListActivityLogsSince(ctx context.Context, since time.Time, maxRows int32) ([]db.ActivityLog, error)

ListActivityLogsSince returns activity_log rows created on or after since, scoped to the configured workspace. Results are ordered created_at ASC.

func (*GTDStore) ListRecentAutomation

func (s *GTDStore) ListRecentAutomation(ctx context.Context, limit int32) ([]db.ActivityLog, error)

ListRecentAutomation returns recent automation activity_log rows, filtered Go-side by sqliteAutomationActions, workspace-scoped. Query: WHERE created_at >= NOW()-7d ORDER BY created_at DESC LIMIT 200, then filter in Go, then slice to limit.

func (*GTDStore) LogActivity

func (s *GTDStore) LogActivity(ctx context.Context, actor, action string, projectID *uuid.UUID, notes string) error

LogActivity records an activity log entry. project may be nil.

func (*GTDStore) ProjectByName

func (s *GTDStore) ProjectByName(ctx context.Context, name string) (*db.Project, error)

ProjectByName looks up a single project by unique name within the workspace.

func (*GTDStore) ProjectsByRepoName

func (s *GTDStore) ProjectsByRepoName(ctx context.Context, repoName string) ([]db.Project, error)

ProjectsByRepoName returns every project whose `repo_name` column matches the given repo, scoped to the configured workspace. Empty repoName → empty slice (fast-path; avoids a wildcard scan). Empty result is not an error.

func (*GTDStore) ProjectsFiltered

func (s *GTDStore) ProjectsFiltered(ctx context.Context, status string) ([]db.Project, error)

ProjectsFiltered returns projects matching status, scoped to the configured workspace. Status "" or "active" → active only, ordered identically to ListActiveProjects (priority ASC, updated_at DESC); "all" → every status; any other value → exact match. Mirrors gtd.Store.ProjectsFiltered (Postgres) and TasksFiltered's switch-by-status pattern.

func (*GTDStore) PruneOlderThan

func (s *GTDStore) PruneOlderThan(ctx context.Context, cutoff time.Time) (int64, error)

PruneOlderThan hard-deletes activity_log rows created before cutoff. Global cleanup (no workspace filter) — matches the Postgres Store.

func (*GTDStore) PullForwardTasks

func (s *GTDStore) PullForwardTasks(ctx context.Context, refDate time.Time) ([]db.Task, error)

PullForwardTasks mirrors the Postgres-side gtd.Store.PullForwardTasks: up to gtd.PullForwardCap pending/in_progress, importance=1 tasks whose due_date is NULL or falls on/after "tomorrow" (midnight, Asia/Taipei, relative to refDate — see gtd.PullForwardTomorrowStart, shared with the Postgres backend so the two cannot drift). SQLite stores due_date as RFC3339Nano TEXT (UTC), so lexicographic string comparison against the tomorrow-start boundary (itself converted to UTC before formatting) is equivalent to a timestamp comparison — the same invariant UpcomingTasks relies on above.

func (*GTDStore) RecentActivityByProject

func (s *GTDStore) RecentActivityByProject(
	ctx context.Context, projectID uuid.UUID, since time.Time, maxRows int32,
) ([]db.ActivityLog, error)

RecentActivityByProject returns activity_log rows for a project since the given timestamp, scoped to the configured workspace, newest first. SQLite parity with gtd.Store.RecentActivityByProject.

func (*GTDStore) RecentCompletedTasks

func (s *GTDStore) RecentCompletedTasks(ctx context.Context, projectID uuid.UUID, limit int32) ([]db.Task, error)

RecentCompletedTasks returns recently-completed tasks for a project, scoped to the configured workspace, ordered by updated_at DESC. SQLite parity with gtd.Store.RecentCompletedTasks.

func (*GTDStore) Tasks

func (s *GTDStore) Tasks(ctx context.Context, projectID *uuid.UUID) ([]db.Task, error)

Tasks returns pending/in-progress tasks, optionally filtered by projectID.

func (*GTDStore) TasksByDueDateRange

func (s *GTDStore) TasksByDueDateRange(ctx context.Context, from, to time.Time) ([]db.Task, error)

TasksByDueDateRange returns pending / in_progress tasks whose due_date falls inside [from, to] (inclusive on both ends), scoped to the configured workspace. The status filter intentionally excludes 'completed' so the calendar planning view shows only work that still needs to happen.

SQLite stores due_date as RFC3339 TEXT (see CreateTask), so range filters rely on lexicographic comparison — RFC3339 sorts identically to chronologic order at the same UTC offset, which CreateTask enforces (.UTC().Format(...)).

func (*GTDStore) TasksByProjectAllStatuses

func (s *GTDStore) TasksByProjectAllStatuses(ctx context.Context, projectID uuid.UUID) ([]db.Task, error)

TasksByProjectAllStatuses returns every task in the project regardless of status, ordered by COALESCE(updated_at, created_at) DESC. Mirrors the Postgres-side gtd.Store.TasksByProjectAllStatuses; both back the `?status=all` variant of the project-detail tasks endpoint.

func (*GTDStore) TasksFiltered

func (s *GTDStore) TasksFiltered(ctx context.Context, f gtd.TaskFilter) ([]db.Task, error)

TasksFiltered returns tasks matching the given gtd.TaskFilter with pagination. It is the SQLite backend for the list_tasks MCP tool. The existing Tasks method is left untouched so its non-test callers keep active-only semantics.

Status "" or "active" → pending+in_progress; "all" → every task status; any other value → exact match. Callers pass Limit+1 to detect has_more without a COUNT query.

func (*GTDStore) TasksForTimeline

func (s *GTDStore) TasksForTimeline(ctx context.Context, from, to time.Time) ([]db.Task, error)

TasksForTimeline returns all tasks (any status) where created_at OR (status='completed' AND updated_at) falls inside [from, to] (inclusive), scoped to the configured workspace. Mirrors the Postgres-side gtd.Store.TasksForTimeline; both back the timeline aggregator's historical task_created / task_completed event query.

SQLite stores timestamps as RFC3339 TEXT, so range filters rely on lexicographic comparison — RFC3339 sorts identically to chronologic order at the same UTC offset, which CreateTask enforces (.UTC().Format(...)).

func (*GTDStore) TopPendingTask

func (s *GTDStore) TopPendingTask(ctx context.Context) (*db.Task, error)

TopPendingTask returns the single highest-priority pending task in the configured workspace, ordered by priority ASC NULLS LAST, importance ASC NULLS LAST, created_at ASC. Returns nil, nil when no pending task exists.

func (*GTDStore) UpcomingTasks

func (s *GTDStore) UpcomingTasks(ctx context.Context, refDate time.Time, days, limit int) ([]db.Task, error)

UpcomingTasks returns pending/in_progress tasks relevant to the upcoming window rooted at refDate. The window includes:

  • tasks with a due_date <= windowEnd (refDate + days, end-of-day UTC)
  • tasks with no due_date, regardless of importance (unscheduled bucket; priority-ordered so high-importance surfaces first)

SQLite stores timestamps as RFC3339 TEXT; lexicographic comparison works because CreateTask enforces .UTC().Format(time.RFC3339Nano) on due_date.

func (*GTDStore) UpdateChecklistItem

func (s *GTDStore) UpdateChecklistItem(
	ctx context.Context, taskID uuid.UUID, workspaceID uuid.UUID,
	itemID uuid.UUID, update gtd.UpdateChecklistItemParams,
) ([]gtd.ChecklistItem, error)

UpdateChecklistItem applies a partial patch to the checklist item identified by itemID inside the given task. Returns the full updated checklist. Returns gtd.ErrNotFound when task or item is not found.

The load+save pair runs inside a BEGIN IMMEDIATE transaction to prevent lost updates when concurrent callers modify the same task's checklist.

func (*GTDStore) UpdateGoal

func (s *GTDStore) UpdateGoal(ctx context.Context, id uuid.UUID, p gtd.UpdateGoalParams) (*db.Goal, error)

UpdateGoal performs a full update of a goal by ID, replacing all mutable fields.

func (*GTDStore) UpdateProject

func (s *GTDStore) UpdateProject(ctx context.Context, id uuid.UUID, p gtd.UpdateProjectParams) (*db.Project, error)

UpdateProject performs a full update of a project by ID, replacing all mutable fields. RepoName semantics: nil → preserve existing DB value; non-nil → overwrite (empty string clears to NULL). Two query branches avoid a gap in parameter positions that would confuse SQLite's positional binding.

func (*GTDStore) UpdateProjectStatus

func (s *GTDStore) UpdateProjectStatus(ctx context.Context, id uuid.UUID, status gtd.ProjectStatus) (*db.Project, error)

UpdateProjectStatus sets the status of a project.

func (*GTDStore) UpdateTask

func (s *GTDStore) UpdateTask(ctx context.Context, id uuid.UUID, p gtd.UpdateTaskParams) (*db.Task, error)

UpdateTask performs a partial update of a task by ID. nil fields in p are preserved from the existing row (no null-clear support). Pre-reads the existing task to fill nil params, then executes a single UPDATE. Returns ErrNotFound when no row matching id exists in the configured workspace.

func (*GTDStore) UpdateTaskStatus

func (s *GTDStore) UpdateTaskStatus(ctx context.Context, id uuid.UUID, status gtd.TaskStatus) (*db.Task, error)

UpdateTaskStatus sets the status of a task.

func (*GTDStore) WeeklyProgress

func (s *GTDStore) WeeklyProgress(ctx context.Context) (completed, total int64, err error)

WeeklyProgress returns completed-this-week and total-week-relevant counts. total = tasks completed this week + pending/in_progress due this week or created this week.

func (*GTDStore) WorkspaceID

func (s *GTDStore) WorkspaceID() pgtype.UUID

WorkspaceID returns the configured workspace UUID for parity with gtd.Store.WorkspaceID(). Used by MCP system_health to surface the active scope. Empty configured workspace → zero pgtype.UUID (Valid=false).

type KnowledgeStore

type KnowledgeStore struct {
	// contains filtered or unexported fields
}

KnowledgeStore is the SQLite-backed implementation of knowledge.StoreIface.

func NewKnowledgeStore

func NewKnowledgeStore(d *DB) *KnowledgeStore

NewKnowledgeStore wraps an open DB into a KnowledgeStore.

func (*KnowledgeStore) AddItem

AddItem creates a knowledge item using LIKE/search-only SQLite v2 semantics. When p.Content contains ATX Markdown headings and p.ParentID is nil, it also inserts child rows for each section (fan-out). Fan-out failures are non-fatal.

Delegates the dedup phase to Prepare and the write to insertItemRow (both extracted so proposal.AcceptOrchestration can reuse them split across its PrepareOutOfBand/tx-scoped Materialize steps — see Prepare's doc comment).

func (*KnowledgeStore) DB

func (s *KnowledgeStore) DB() *DB

DB returns the underlying *DB, mirroring ProposalStore.DB(). Exported so callers outside this package (e.g. internal/storage's future AcceptSeam, and this package's own tests in package sqlite_test) can open a *sql.Tx via DB().BeginTx(ctx) to drive WriteItemTx.

func (*KnowledgeStore) GetByID

func (s *KnowledgeStore) GetByID(ctx context.Context, id uuid.UUID) (*db.KnowledgeItem, error)

GetByID returns a single knowledge item by ID within the workspace scope.

func (*KnowledgeStore) List

func (s *KnowledgeStore) List(ctx context.Context, limit, offset int) ([]db.KnowledgeItem, error)

List returns knowledge items ordered by creation date.

func (*KnowledgeStore) ListByProjectID

func (s *KnowledgeStore) ListByProjectID(ctx context.Context, projectID uuid.UUID, limit int) ([]db.KnowledgeItem, error)

ListByProjectID returns knowledge items associated with a project UUID. Added in migration 000049; no FK constraint (CLAUDE.md red-line §9). SECURITY: workspace-scoped.

func (*KnowledgeStore) ListByTaskID

func (s *KnowledgeStore) ListByTaskID(ctx context.Context, taskID uuid.UUID, limit int) ([]db.KnowledgeItem, error)

ListByTaskID returns knowledge items associated with a task UUID. Added in migration 000049; no FK constraint (CLAUDE.md red-line §9). SECURITY: workspace-scoped.

func (*KnowledgeStore) ListChildren

func (s *KnowledgeStore) ListChildren(ctx context.Context, parentID uuid.UUID) ([]*db.KnowledgeItem, error)

ListChildren returns direct children of parentID ordered by heading_level, created_at. Used by navigate_knowledge and outline_knowledge MCP tools.

func (*KnowledgeStore) ListRoots

func (s *KnowledgeStore) ListRoots(ctx context.Context) ([]*db.KnowledgeItem, error)

ListRoots returns top-level items (parent_id IS NULL) ordered by creation date descending. Used by navigate_knowledge when no parent_id is supplied.

func (*KnowledgeStore) Prepare

Prepare runs AddItem's out-of-band pre-write phase for SQLite: URL exact-match dedup only (top-level items, mirroring AddItem's existing p.ParentID == nil guard). SQLite has no embedding client wired up yet — see knowledge.PreparedItem.Vec's doc comment — so no cosine-similarity dedup runs here (that lands with A10-dedup); Vec is always nil and DedupSkipped is always true. Extracted so proposal.AcceptOrchestration (ADR 0003's dual-backend orchestration seam) can run it strictly before BeginTx, mirroring the Postgres knowledge.Store.Prepare.

func (*KnowledgeStore) Search

func (s *KnowledgeStore) Search(ctx context.Context, query string, limit int) ([]db.KnowledgeItem, error)

Search performs FTS5 full-text search over title and content, sorted app-side by Ebbinghaus strength. On each hit, recall_count is incremented atomically.

func (*KnowledgeStore) SearchByCosine

func (s *KnowledgeStore) SearchByCosine(ctx context.Context, queryEmbedding []float32, limit int) ([]db.KnowledgeItem, error)

SearchByCosine returns the top-limit knowledge items most similar to queryEmbedding. SQLite has no pgvector — brute-force Go-side cosine scan. knowledge_items.embedding is stored as BLOB (serialized float32 LE).

SECURITY: filtered by workspace_id — no cross-workspace data returned.

func (*KnowledgeStore) SearchCoarse

func (s *KnowledgeStore) SearchCoarse(ctx context.Context, query string, limit int) ([]db.KnowledgeItem, error)

SearchCoarse searches only root-level rows (parent_id IS NULL, COALESCE(heading_level, 0) = 0) using FTS5. Used by search_knowledge mode="coarse".

func (*KnowledgeStore) SearchReadOnly

func (s *KnowledgeStore) SearchReadOnly(ctx context.Context, query string, limit int) ([]db.KnowledgeItem, error)

SearchReadOnly performs the identical FTS5+Ebbinghaus search as Search but never mutates knowledge_items — no recall_count/last_recalled_at bump. PG+SQLite parity: mirrors internal/knowledge/store.go SearchReadOnly. Used by contextpack.Assembler.retrieveKnowledge so assemble_context stays genuinely read-only.

func (*KnowledgeStore) SoftPruneDecayed

func (s *KnowledgeStore) SoftPruneDecayed(ctx context.Context, cutoff time.Time, strengthThreshold float64) (int64, error)

SoftPruneDecayed implements decay.PrunerStore for the SQLite backend. It sets archived_at=NOW() on knowledge_items that are:

  • not already archived (archived_at IS NULL)
  • older than cutoff (created_at < cutoff, i.e. age > 90 days)
  • Ebbinghaus strength (computed app-side) < strengthThreshold

Decisions table is never touched.

func (*KnowledgeStore) UpdateEmbedding

func (s *KnowledgeStore) UpdateEmbedding(ctx context.Context, id uuid.UUID, embedding []byte) error

UpdateEmbedding writes the embedding bytes to the knowledge_items row matching id within the current workspace scope. Best-effort: returns nil when no row matches. Used by tests + the future Stop-hook integration that will populate knowledge_items.embedding alongside session_handoffs.

func (*KnowledgeStore) UpdateEmbeddingTx

func (s *KnowledgeStore) UpdateEmbeddingTx(ctx context.Context, tx *sql.Tx, id uuid.UUID, embedding []byte) error

UpdateEmbeddingTx is the tx-scoped twin of UpdateEmbedding, used by WriteItemTx so the embedding write lands in the same transaction as the row insert instead of racing it as a separate connection acquisition (see WriteItemTx's doc comment for the single-connection deadlock this avoids).

func (*KnowledgeStore) UpdateLearningValue

func (s *KnowledgeStore) UpdateLearningValue(ctx context.Context, id uuid.UUID, value int) error

UpdateLearningValue sets the star-rating (1–5) for the given knowledge item. Returns knowledge.ErrNotFound when no row matches within the workspace scope.

func (*KnowledgeStore) WriteItemTx

func (s *KnowledgeStore) WriteItemTx(ctx context.Context, tx *sql.Tx, prep knowledge.PreparedItem) (*db.KnowledgeItem, error)

WriteItemTx inserts the row described by prep.Params inside the given open tx, then re-reads it back through the SAME tx. Reading back through s.db.conn while tx is open would deadlock: SQLite serialises all access through a single pooled connection (db.SetMaxOpenConns(1) in Open), and that connection is already held by tx — see the identical constraint documented on sqliteAcceptAdapter.getPendingTx (accept_proposal.go). Writes the embedding via UpdateEmbeddingTx only when prep.Vec is non-nil; SQLite's Prepare never sets one today (see knowledge.PreparedItem.Vec's doc comment), so this branch is presently unreachable in production until A10-dedup wires an embed client, but WriteItemTx honours prep.Vec regardless of who set it — parity with the Postgres knowledge.Store.WriteItemTx. Used by internal/storage/sqlite/accept_proposal.go's sqliteAcceptAdapter.Materialize (wired by the A1-seam task, not this one).

type LearningStore

type LearningStore struct {
	// contains filtered or unexported fields
}

LearningStore is the SQLite-backed implementation of learning.StoreIface.

func NewLearningStore

func NewLearningStore(d *DB) *LearningStore

NewLearningStore wraps an open DB into a LearningStore.

func (*LearningStore) CountDueReviews

func (s *LearningStore) CountDueReviews(ctx context.Context) (int, error)

CountDueReviews returns the total number of concepts currently due.

func (*LearningStore) CreateConcept

func (s *LearningStore) CreateConcept(ctx context.Context, title, content string, tags []string) (*db.Concept, error)

CreateConcept inserts a concept and its initial review schedule.

func (*LearningStore) CreateConceptTx

func (s *LearningStore) CreateConceptTx(ctx context.Context, tx *sql.Tx, title, content string, tags []string) (uuid.UUID, error)

CreateConceptTx inserts a concept and its initial review_schedule row within the provided *sql.Tx. It is the transactional counterpart of CreateConcept and is used by the confirm_proposal accept path so that concept creation and proposal resolution are committed atomically.

func (*LearningStore) DueReviews

func (s *LearningStore) DueReviews(ctx context.Context, limit int) ([]learning.DueReview, error)

DueReviews returns concepts whose review schedule is due and status is active.

func (*LearningStore) GetScheduleState

func (s *LearningStore) GetScheduleState(ctx context.Context, scheduleID uuid.UUID) (learning.CardState, error)

GetScheduleState returns the current CardState for scheduleID, scoped to the store's configured workspace. Sibling of the Postgres store's GetScheduleState (Ω7 fix) — see internal/learning/iface.go for the full rationale.

func (*LearningStore) LearningStats

func (s *LearningStore) LearningStats(ctx context.Context) (*learning.LearningStatsResult, error)

LearningStats returns aggregate learning stats for the workspace.

func (*LearningStore) ListConcepts

func (s *LearningStore) ListConcepts(ctx context.Context, limit int) ([]db.Concept, error)

ListConcepts returns up to limit concepts ordered by created_at DESC, scoped to the configured workspace.

func (*LearningStore) ListForAIReview

func (s *LearningStore) ListForAIReview(ctx context.Context, minReviewCount int) ([]learning.ConceptForReview, error)

ListForAIReview returns active concepts with at least minReviewCount completed reviews, ordered by review count descending.

func (*LearningStore) ReviewHistory

func (s *LearningStore) ReviewHistory(ctx context.Context) ([]learning.ConceptHistoryRow, error)

ReviewHistory returns all non-archived concepts joined with their review schedule, sorted by last_review_at DESC NULLS LAST. interval_days is approximated as stability * 9 (same FSRS formula as the Postgres store), since the review_schedule table has no interval_days column.

func (*LearningStore) ReviewedSince

func (s *LearningStore) ReviewedSince(ctx context.Context, since time.Time, limit int) ([]learning.DueReview, error)

ReviewedSince returns DueReview entries whose last_review_at >= since, scoped to the configured workspace.

func (*LearningStore) SoftPruneDecayed

func (s *LearningStore) SoftPruneDecayed(ctx context.Context, cutoff time.Time, strengthThreshold float64) (int64, error)

SoftPruneDecayed implements decay.PrunerStore for the SQLite concepts table. It sets archived_at=NOW() on concepts that are:

  • not already archived (archived_at IS NULL)
  • older than cutoff (created_at < cutoff, i.e. age > 90 days)
  • Ebbinghaus strength (computed app-side) < strengthThreshold

Decisions table is never touched.

func (*LearningStore) SubmitReview

func (s *LearningStore) SubmitReview(
	ctx context.Context, scheduleID uuid.UUID, currentState learning.CardState, rating learning.Rating,
) error

SubmitReview applies FSRS and updates the review schedule.

func (*LearningStore) UpdateConceptStatus

func (s *LearningStore) UpdateConceptStatus(ctx context.Context, id uuid.UUID, status string) error

UpdateConceptStatus sets the status column for the given concept.

type OutcomeStore

type OutcomeStore struct {
	// contains filtered or unexported fields
}

OutcomeStore is the SQLite-backed implementation of outcome.StoreIface.

func NewOutcomeStore

func NewOutcomeStore(d *DB) *OutcomeStore

NewOutcomeStore wraps an open DB into an OutcomeStore.

func (*OutcomeStore) CreateEvaluation

func (s *OutcomeStore) CreateEvaluation(ctx context.Context, params outcome.CreateEvaluationParams) (outcome.Evaluation, error)

CreateEvaluation inserts a new evaluation row and returns the persisted record.

func (*OutcomeStore) CreateOutcome

func (s *OutcomeStore) CreateOutcome(ctx context.Context, params outcome.CreateOutcomeParams) (outcome.Outcome, error)

CreateOutcome inserts a new outcome row and returns the persisted record.

func (*OutcomeStore) ExistsForEntity

func (s *OutcomeStore) ExistsForEntity(ctx context.Context, workspaceID *uuid.UUID, entityType string, entityID uuid.UUID) (bool, error)

ExistsForEntity reports whether an outcome row already exists for the given entity, optionally scoped to workspaceID. Mirrors the outcome.Store (PG) implementation.

func (*OutcomeStore) FinalizeDraft

func (s *OutcomeStore) FinalizeDraft(ctx context.Context, id uuid.UUID, params outcome.CreateOutcomeParams) (outcome.Outcome, error)

FinalizeDraft transitions a result='unknown' draft to (usually) a terminal result in place. Mirrors outcome.Store (PG)'s append-only merge semantics (migration 000075, user-directed redesign — see that Store's own doc comment for the full field-by-field rule and threat-model rationale): only Result and UpdatedAt are unconditionally overwritten; Notes/Metrics/ RelatedRuleIDs/WorkSessionID are append-only and can never lose existing content.

Unlike the PG version, this merge is computed in Go rather than in SQL (SQLite's metrics/related_rule_ids columns are TEXT-encoded JSON, not native jsonb/array types — modernc.org/sqlite v1.50.0 DOES have JSON1 compiled in, including json_patch, verified empirically, but doing the merge in Go keeps this in one place with the transaction below rather than splitting logic between SQL functions and Go transaction control; "兩後端 語意等價,寫法可以不同" — see dispatch). That requires reading the existing row before computing the merged values, which (MIN-2 fix) is wrapped in a single BEGIN IMMEDIATE-equivalent serializable transaction — the same lost-update-prevention pattern already used by gtd.go's AddChecklistItem/UpdateChecklistItem — so no concurrent writer can be observed between the read and the write. The final UPDATE... RETURNING (modernc.org/sqlite supports RETURNING, verified empirically) reads back the committed row in the SAME statement, closing the two-separate-statements race the old Exec-then-getByID pattern had (a concurrent writer's content could previously leak into what this call returned as "what I wrote").

related_rule_ids is additionally capped at outcome.MaxRelatedRuleIDsTotal via outcome.CapRelatedRuleIDs AFTER unionRelatedRuleIDs computes the full merge (PR #152 round 5 Major M-R5-1 guarantee A; widened in round 6 Major m-R6-3 to also cover the case where existing already exceeds the cap — see CapRelatedRuleIDs's doc comment for the full rule and the cross-backend-disagreement bug this closes). Notes is likewise capped at outcome.MaxNotesTotalRunes via outcome.CapNotesTotal (PR #152 round 6 Major M-R6-2 guarantee B) — both cap helpers live in internal/outcome/store.go so the SAME algorithm backs both this Go implementation and Store (PG)'s SQL equivalent, verified byte-identical by this file's and store_test.go's parity tests.

Workspace-scoped (PR #152 round 8, 80cf80b6 finding 3): mirrors outcome.Store.FinalizeDraft's PG fix — the SELECT and UPDATE below both now carry `(?N IS NULL OR workspace_id = ?N)` alongside `id = ...`, the same unscoped-when-nil pattern GetLatestForEntity already uses elsewhere in this file. params.WorkspaceID == nil (every pre-existing FinalizeDraft test in outcome_test.go) still matches any row, unchanged from before; a mismatched non-nil WorkspaceID now makes both statements match zero rows, surfacing as ErrDraftAlreadyFinalized instead of writing into another workspace's draft.

func (*OutcomeStore) GetLatestForEntity

func (s *OutcomeStore) GetLatestForEntity(
	ctx context.Context, workspaceID *uuid.UUID, entityType string, entityID uuid.UUID,
) (outcome.Outcome, error)

GetLatestForEntity returns the most recently created outcome for the given entity, workspace-scoped when non-nil. Returns outcome.ErrNotFound when no outcome exists yet. Mirrors the outcome.Store (PG) implementation.

ORDER BY carries a second key, id DESC, to break ties when two rows share a bit-identical created_at (security-review-reproduced: SQLite's TEXT created_at has millisecond granularity and ties are easy to hit in fast call sequences). Without this, "the latest outcome" was non-deterministic on a tie, which could resolve to an older row and make a newly-seeded draft permanently unreachable — every subsequent record_outcome call for that entity then hard-fails against idx_outcomes_one_open_draft. `id DESC` mirrors the tie-break migrations/sqlite/000074_outcomes_supersession.up.sql's dedup step already uses, so both the one-time dedup and every live read agree on which row wins a tie.

func (*OutcomeStore) GetOutcomeByID

func (s *OutcomeStore) GetOutcomeByID(ctx context.Context, id uuid.UUID, workspaceID *uuid.UUID) (outcome.Outcome, error)

GetOutcomeByID fetches a single outcome by primary key, workspace-scoped.

func (*OutcomeStore) ListEvaluationsByOutcomeID

func (s *OutcomeStore) ListEvaluationsByOutcomeID(
	ctx context.Context, outcomeID uuid.UUID, workspaceID *uuid.UUID,
) ([]outcome.Evaluation, error)

ListEvaluationsByOutcomeID returns all evaluations for an outcome, ordered by created_at ASC.

func (*OutcomeStore) ListFailedOutcomes

func (s *OutcomeStore) ListFailedOutcomes(ctx context.Context, workspaceID *uuid.UUID, limit int) ([]outcome.Outcome, error)

ListFailedOutcomes returns outcomes with result='failure' or result='regressed'.

func (*OutcomeStore) ListRecentOutcomes

func (s *OutcomeStore) ListRecentOutcomes(
	ctx context.Context, workspaceID *uuid.UUID, entityType string, limit int,
) ([]outcome.Outcome, error)

ListRecentOutcomes returns outcomes ordered by created_at DESC, with optional workspace and entity_type filters.

func (*OutcomeStore) PruneOlderThan

func (s *OutcomeStore) PruneOlderThan(ctx context.Context, cutoff time.Time) (int64, error)

PruneOlderThan hard-deletes outcomes and their evaluations older than cutoff. Evaluations are deleted first (no FK cascade per red-line §9).

func (*OutcomeStore) SeedDraft

func (s *OutcomeStore) SeedDraft(
	ctx context.Context, workspaceID *uuid.UUID, entityType string, entityID uuid.UUID,
) (outcome.Outcome, bool, error)

SeedDraft atomically ensures a result='unknown' draft exists for the given entity. See outcome.StoreIface.SeedDraft doc comment for the full semantics. Uses ExecContext + RowsAffected (rather than a RETURNING clause) for the INSERT ... ON CONFLICT DO NOTHING step, consistent with this file's other upsert call sites (e.g. worksession.go's work_session_tasks link insert).

type PlaybookStore

type PlaybookStore struct {
	// contains filtered or unexported fields
}

PlaybookStore is the SQLite-backed implementation of playbook.StoreIface.

func NewPlaybookStore

func NewPlaybookStore(d *DB) *PlaybookStore

NewPlaybookStore wraps an open DB into a PlaybookStore.

func (*PlaybookStore) Create

Create inserts a new playbook and returns the persisted record.

func (*PlaybookStore) IncrementHits

func (s *PlaybookStore) IncrementHits(ctx context.Context, id uuid.UUID) error

IncrementHits atomically increments the hits counter and sets last_used_at.

func (*PlaybookStore) List

List returns playbooks matching the filter, ordered by confidence DESC.

type ProceduralStore

type ProceduralStore struct {
	// contains filtered or unexported fields
}

ProceduralStore is the SQLite-backed implementation of procedural.StoreIface.

func NewProceduralStore

func NewProceduralStore(d *DB) *ProceduralStore

NewProceduralStore wraps an open DB into a ProceduralStore.

func (*ProceduralStore) Add

Add inserts a new procedural memory and returns the persisted record.

func (*ProceduralStore) MarkUsed

MarkUsed increments success_count and sets last_used_at = now.

func (*ProceduralStore) Query

Query returns procedural memories matching the filter.

type ProposalStore

type ProposalStore struct {
	// contains filtered or unexported fields
}

ProposalStore is the SQLite-backed implementation of proposal.StoreIface.

func NewProposalStore

func NewProposalStore(d *DB) *ProposalStore

NewProposalStore wraps an open DB into a ProposalStore.

func (*ProposalStore) AutoProposeConceptFromKnowledge

func (s *ProposalStore) AutoProposeConceptFromKnowledge(
	ctx context.Context, item *db.KnowledgeItem, proposedBy string,
) (*db.PendingProposal, error)

AutoProposeConceptFromKnowledge creates a pending concept proposal from a knowledge item when the item type is suitable for spaced repetition.

func (*ProposalStore) BatchConfirm

func (s *ProposalStore) BatchConfirm(ctx context.Context, ids []uuid.UUID, status proposal.Status) (proposal.BatchConfirmResult, error)

BatchConfirm resolves multiple proposals independently (best-effort). Each ID is processed in its own implicit SQLite transaction so a failure for one ID does not roll back the others. The caller is responsible for input validation (ids length 1–100, valid status).

func (*ProposalStore) Create

Create records a new pending proposal.

func (*ProposalStore) DB

func (s *ProposalStore) DB() *DB

DB returns the underlying *DB so callers in the mcp package can begin a cross-store transaction via DB().BeginTx without the ProposalStore needing to hold references to sibling stores.

func (*ProposalStore) Get

Get returns a single proposal by ID.

func (*ProposalStore) ImportProposal

func (s *ProposalStore) ImportProposal(ctx context.Context, p db.PendingProposal) error

ImportProposal inserts a pending_proposals row using p's own id/status/created_at/resolved_at instead of generating fresh ones, so a copy from production preserves resolution history (accepted/rejected proposals, not just pending ones). Used by cmd/qa-seed. Fails (no upsert) on a duplicate id — callers MUST import into a fresh database.

func (*ProposalStore) ListAll

func (s *ProposalStore) ListAll(ctx context.Context, proposalType string, limit int32) ([]db.PendingProposal, error)

ListAll returns all proposals of the given type regardless of status, newest first, up to limit rows. Status filtering is done in Go by the caller. Using parameterized query prevents SQL injection.

func (*ProposalStore) ListPending

func (s *ProposalStore) ListPending(ctx context.Context) ([]db.PendingProposal, error)

ListPending returns all pending proposals, newest first.

func (*ProposalStore) MarkAndDeleteStaleProposals

func (s *ProposalStore) MarkAndDeleteStaleProposals(
	ctx context.Context, taskRetention, decisionRetention, resolvedRetention time.Duration, markReason string,
) (markedRows, deletedRows int64, err error)

MarkAndDeleteStaleProposals is the SQLite-native counterpart to scheduler.go's Postgres two-step runDailyPendingProposalsPrune logic (internal/scheduler/scheduler.go): (1) mark pending type='task' proposals older than taskRetention as status='rejected' with reason markReason, then (2) delete resolved (accepted/rejected) rows older than resolvedRetention and pending type='decision' rows older than decisionRetention. Other pending types (goal/project/concept/knowledge/playbook) are NEVER touched by either step — same "unresolved user intent" boundary the Postgres job documents.

SQLite has no `NOW() - INTERVAL` syntax (GTD decision G4, 6ea0b014 — same rationale as every other SQLite cognitive-job adapter in this package), so all three cutoffs are computed in Go from time.Now().UTC() and bound as parameters instead of interval literals.

Deliberately NOT workspace-scoped, matching the Postgres job's actual behaviour (it prunes stale scheduler proposals across every workspace, not just one).

A mark-step failure does NOT block the delete step (mirrors the Postgres job: "the mark step's outcome does not gate the delete step" — a transient mark failure shouldn't block the resolved-row cleanup). Returns (markedRows, deletedRows, err); err is errors.Join(markErr, delErr) so a caller can log both failures if both steps fail independently.

func (*ProposalStore) Resolve

func (s *ProposalStore) Resolve(ctx context.Context, id uuid.UUID, status proposal.Status) (*db.PendingProposal, error)

Resolve marks a pending proposal as accepted or rejected.

func (*ProposalStore) ResolveTx

func (s *ProposalStore) ResolveTx(ctx context.Context, tx *sql.Tx, id uuid.UUID, status proposal.Status) error

ResolveTx marks a pending proposal as accepted or rejected within an existing *sql.Tx. Used by the confirm_proposal accept path to wrap proposal-resolve plus entity-materialise in a single atomic transaction.

type ReflectionStore

type ReflectionStore struct {
	// contains filtered or unexported fields
}

ReflectionStore is the SQLite-backed implementation of reflection.StoreIface.

func NewReflectionStore

func NewReflectionStore(d *DB) *ReflectionStore

NewReflectionStore wraps an open DB into a ReflectionStore.

func (*ReflectionStore) ByRelatedEntity

func (s *ReflectionStore) ByRelatedEntity(
	ctx context.Context, workspaceID *uuid.UUID, entityType string, entityID uuid.UUID, limit int,
) ([]*reflection.Reflection, error)

ByRelatedEntity returns reflections scoped to a specific related entity.

func (*ReflectionStore) Create

Create inserts a new reflection and returns the persisted record.

func (*ReflectionStore) GetLatest

func (s *ReflectionStore) GetLatest(ctx context.Context, workspaceID *uuid.UUID, reflType string) (*reflection.Reflection, error)

GetLatest returns the most recent reflection of a given type in the workspace.

func (*ReflectionStore) List

List returns reflections matching the filter, ordered by created_at DESC.

func (*ReflectionStore) PruneOlderThan

func (s *ReflectionStore) PruneOlderThan(ctx context.Context, cutoff time.Time) (int64, error)

PruneOlderThan hard-deletes reflection rows with created_at < cutoff. Called daily by the scheduler to enforce the 180-day TTL per backend-security-design.md §1.3.

func (*ReflectionStore) RecentWithPatterns

func (s *ReflectionStore) RecentWithPatterns(
	ctx context.Context, workspaceID *uuid.UUID, since time.Time, limit int,
) ([]*reflection.Reflection, error)

RecentWithPatterns returns reflections that have a non-null patterns_detected value and were created on or after since, ordered by created_at DESC.

type SessionStore

type SessionStore struct {
	// contains filtered or unexported fields
}

SessionStore is the SQLite-backed implementation of session.StoreIface.

func NewSessionStore

func NewSessionStore(d *DB) *SessionStore

NewSessionStore wraps an open DB into a SessionStore.

func (*SessionStore) HandoffsByRepo

func (s *SessionStore) HandoffsByRepo(ctx context.Context, repoName string, limit int) ([]db.SessionHandoff, error)

HandoffsByRepo returns recent handoffs whose repo_name matches repoName, scoped to the configured workspace, newest first. SQLite parity with session.Store.HandoffsByRepo.

func (*SessionStore) HandoffsSince

func (s *SessionStore) HandoffsSince(ctx context.Context, since time.Time, limit int) ([]db.SessionHandoff, error)

HandoffsSince returns handoffs created or resolved on or after since, scoped to the configured workspace. Used by the timeline aggregator.

func (*SessionStore) LatestHandoff

func (s *SessionStore) LatestHandoff(ctx context.Context) (*db.SessionHandoff, error)

LatestHandoff returns the most recent unresolved handoff, or session.ErrNotFound. ORDER BY created_at DESC, rowid DESC: rowid is SQLite's monotonically-increasing internal row counter and acts as a stable tiebreaker when two rows share the same created_at millisecond (possible after the timestamp format change to .000).

func (*SessionStore) MarkNextActionDone

func (s *SessionStore) MarkNextActionDone(ctx context.Context, handoffID uuid.UUID, step int) (*db.SessionHandoff, error)

MarkNextActionDone sets next_actions[step].status = "done" for the handoff identified by id, scoped to the configured workspace. Returns session.ErrNotFound when no matching handoff exists in the workspace.

SECURITY: workspace isolation enforced — id is validated against workspace_id.

func (*SessionStore) PruneOlderThan

func (s *SessionStore) PruneOlderThan(ctx context.Context, cutoff time.Time) (int64, error)

PruneOlderThan hard-deletes session_handoffs rows where resolved_at IS NOT NULL and resolved_at < cutoff. Open (unresolved) handoffs are NEVER deleted regardless of age. Matches the Postgres Store.

func (*SessionStore) Resolve

func (s *SessionStore) Resolve(ctx context.Context, id uuid.UUID) error

Resolve marks a handoff as resolved so it will not appear in future queries.

func (*SessionStore) SearchByCosine

func (s *SessionStore) SearchByCosine(ctx context.Context, queryEmbedding []float32, limit int) ([]db.SessionHandoff, error)

SearchByCosine returns the top-limit session handoffs most similar to queryEmbedding. Only rows whose embedding_provider matches the query vector's provider are considered — this prevents cross-provider dimension mismatches (CosineSimilarity returns 0 when dimensions differ). SQLite has no pgvector — brute-force Go-side cosine scan.

SECURITY: filtered by workspace_id — no cross-workspace data returned.

func (*SessionStore) SetHandoff

SetHandoff records a new session handoff for the next session to pick up.

func (*SessionStore) UpdateEmbedding

func (s *SessionStore) UpdateEmbedding(ctx context.Context, embedding []byte) error

UpdateEmbedding writes the embedding bytes to the most recent unresolved session handoff (SQLite version, uses BLOB column). Best-effort.

func (*SessionStore) UpdateEmbeddingByID

func (s *SessionStore) UpdateEmbeddingByID(ctx context.Context, id uuid.UUID, embedding []byte, providerTag string, dim int) error

UpdateEmbeddingByID writes the embedding bytes plus provider metadata to the session handoff with the given ID (SQLite version), scoped to workspace_id. providerTag is "gemini", "hashed", or "unknown"; dim is len(embedding)/4.

func (*SessionStore) UpdateSummary

func (s *SessionStore) UpdateSummary(ctx context.Context, summary string) error

UpdateSummary writes summary to the most recent unresolved handoff's summary_text column. Best-effort: 0 rows affected (no unresolved handoff) is not an error.

type SkillStore

type SkillStore struct {
	// contains filtered or unexported fields
}

SkillStore is the SQLite-backed implementation of skill.StoreIface.

func NewSkillStore

func NewSkillStore(d *DB) *SkillStore

NewSkillStore wraps an open DB into a SkillStore.

func (*SkillStore) Add

Add inserts a new skill and returns the persisted record.

func (*SkillStore) IncrementSuccess

func (s *SkillStore) IncrementSuccess(ctx context.Context, id string, workspaceID *string) (*skill.Skill, error)

IncrementSuccess increments success_count and sets last_used_at = now.

func (*SkillStore) ListRelevant

func (s *SkillStore) ListRelevant(ctx context.Context, workspaceID *string, query string, limit int) ([]*skill.Skill, error)

ListRelevant returns skills ordered by success_count DESC, last_used_at DESC.

func (*SkillStore) Search

func (s *SkillStore) Search(ctx context.Context, f skill.SearchFilter) ([]*skill.Skill, error)

Search returns skills whose name or description match f.Query (LIKE).

func (*SkillStore) UpdateFromOutcome

func (s *SkillStore) UpdateFromOutcome(ctx context.Context, p skill.UpdateFromOutcomeParams, workspaceID *string) (*skill.Skill, error)

UpdateFromOutcome increments the success or failure counter and appends an example entry.

type VisionStore

type VisionStore struct {
	// contains filtered or unexported fields
}

VisionStore is the SQLite-backed implementation of vision.StoreIface.

func NewVisionStore

func NewVisionStore(d *DB) *VisionStore

NewVisionStore wraps an open DB into a VisionStore.

func (*VisionStore) Add

Add inserts a new vision item.

func (*VisionStore) GetByID

func (s *VisionStore) GetByID(ctx context.Context, id uuid.UUID) (*vision.VisionItem, error)

GetByID returns the full vision item by id.

func (*VisionStore) List

List returns vision items matching the filter, ordered by created_at DESC.

func (*VisionStore) Promote

Promote marks a vision item as promoted and records the resulting task id.

func (*VisionStore) Update

Update applies non-nil fields in p to the vision item.

type WorkSessionStore

type WorkSessionStore struct {
	// contains filtered or unexported fields
}

WorkSessionStore is the SQLite-backed implementation of worksession.StoreIface.

func NewWorkSessionStore

func NewWorkSessionStore(d *DB) *WorkSessionStore

NewWorkSessionStore wraps an open DB into a WorkSessionStore.

func (*WorkSessionStore) AddEvidence

AddEvidence inserts one evidence row and returns the persisted record. ev.OutputExcerpt goes through worksession.RedactAndCapOutputExcerpt (redact THEN cap — never the reverse). ev.Command is validated with worksession.CheckControlChars.

func (*WorkSessionStore) Checkpoint

Checkpoint sets status=checkpointed and updates last_checkpoint_at.

func (*WorkSessionStore) Create

Create inserts a new in_progress work session and links task_ids as primary.

func (*WorkSessionStore) Finish

Finish sets status=completed and stores final_summary. After the session update, linked tasks are batch-marked as completed:

  • If FinishParams.CompletedTaskIDs is non-empty, only those tasks are marked.
  • Otherwise, if FinishParams.CompleteAllLinkedTasks is true, all tasks linked via work_session_tasks are marked completed.
  • Otherwise (both empty/false), no tasks are marked completed — Ω5 fix, mirrors the Postgres store (see internal/worksession/store.go).

Returns the updated session and the actual list of task IDs marked completed.

wbt-2.0 P2.2: also persists VerificationStatus/VerificationCommand/ VerificationOutputExcerpt/FinalResult/OutcomeID and, when p.Evidence is non-empty, inserts each evidence row (best-effort, non-fatal on error).

func (*WorkSessionStore) GetActive

func (s *WorkSessionStore) GetActive(
	ctx context.Context, workspaceID uuid.UUID, repoName string,
) (*worksession.ActiveSessionResult, error)

GetActive returns the in_progress session for workspace+repo.

func (*WorkSessionStore) GetByID

func (s *WorkSessionStore) GetByID(ctx context.Context, workspaceID, sessionID uuid.UUID) (*worksession.Session, error)

GetByID returns the session scoped to workspaceID.

func (*WorkSessionStore) GetEvidence

func (s *WorkSessionStore) GetEvidence(ctx context.Context, sessionID uuid.UUID) ([]worksession.Evidence, error)

GetEvidence returns all work_session_evidence rows for sessionID, scoped to the store's configured workspace via a real equality filter (wbt-2.0 security backlog — unlike Checkpoint/Finish/GetByID/ListRecent's `(?N IS NULL OR workspace_id = ?N)` pattern via workspaceArg(), which degenerates to always-true in legacy mode, this filter uses the same zero-UUID-in-legacy-mode value AddEvidence stamps above, so it actually filters even when no WORKSPACE_ID is configured. This intentionally diverges from the rest of the file — see AddEvidence's comment for the defence-in-depth rationale — and from Postgres's worksession.Store, which already used a real equality filter here (this SQLite fix brings the two backends into parity). Defence in depth alongside the caller-side GetByID gate that handleGetWorkSessionTrace already performs before calling this. Row count is hard-capped at worksession.MaxEvidenceListLimit; ordered by created_at ASC so a cap hit drops the newest row(s), not the oldest. Returns an empty (non-nil) slice when no rows exist.

Bug fix (wbt-2.0 review round2 F3): in legacy mode (s.db.workspaceID == ""), the predicate also accepts workspace_id IS NULL. Before the zero-UUID stamping above was introduced, legacy-mode AddEvidence wrote SQL NULL into workspace_id; the strict `workspace_id = ?1` equality filter added here then made those pre-existing rows permanently unreadable (NULL never equals a value in SQL) with no error — silent data loss for anyone who had evidence rows from before that change. Non-legacy mode (a real s.db.workspaceID configured) keeps the strict equality-only filter; NULL rows never belong to a configured workspace and must stay invisible there.

func (*WorkSessionStore) LinkTask

func (s *WorkSessionStore) LinkTask(ctx context.Context, sessionID, taskID uuid.UUID, role string) error

LinkTask attaches a task to a session with the given role.

func (*WorkSessionStore) LinkedTasks

func (s *WorkSessionStore) LinkedTasks(ctx context.Context, sessionID uuid.UUID) ([]worksession.SessionTask, error)

LinkedTasks returns all task links for the given session.

func (*WorkSessionStore) ListRecent

func (s *WorkSessionStore) ListRecent(
	ctx context.Context, workspaceID uuid.UUID, repoName string, limit int,
) ([]worksession.Session, error)

ListRecent returns sessions ordered by created_at DESC, optionally filtered by repoName (empty = no filter). limit is hard-capped at worksession.MaxListRecentLimit (100) regardless of the caller-requested value. Returns an empty (non-nil) slice when the caller's workspaceID does not match the store's configured workspace (mirrors GetActive's silent cross-workspace deny).

func (*WorkSessionStore) PruneOlderThan

func (s *WorkSessionStore) PruneOlderThan(ctx context.Context, cutoff time.Time) (int64, error)

PruneOlderThan hard-deletes work_session_evidence rows older than cutoff. Mirrors outcome.Store.PruneOlderThan exactly: no workspace scoping (this is a maintenance/retention operation, not a per-request isolation boundary).

func (s *WorkSessionStore) SetOutcomeLink(ctx context.Context, sessionID, outcomeID uuid.UUID) error

SetOutcomeLink sets work_sessions.outcome_id for sessionID, scoped to the store's workspace. Returns worksession.ErrNotFound when sessionID does not exist in that workspace.

type WorkspaceStore

type WorkspaceStore struct {
	// contains filtered or unexported fields
}

WorkspaceStore is the SQLite-backed implementation of workspace.StoreIface.

func NewWorkspaceStore

func NewWorkspaceStore(d *DB) *WorkspaceStore

NewWorkspaceStore wraps an open DB into a WorkspaceStore.

func (*WorkspaceStore) ActiveRepos

func (s *WorkspaceStore) ActiveRepos(ctx context.Context) ([]db.Repo, error)

ActiveRepos returns all active repos, ordered by recent activity.

func (*WorkspaceStore) GetModelPreference

func (s *WorkspaceStore) GetModelPreference(ctx context.Context) (string, error)

GetModelPreference returns the stored model_preference, or workspace.DefaultModelPreference when no row exists.

func (*WorkspaceStore) RepoByID

func (s *WorkspaceStore) RepoByID(ctx context.Context, id uuid.UUID) (*db.Repo, error)

RepoByID returns a single repo by primary key UUID, or workspace.ErrNotFound. Workspace-scoped via the configured workspace_id (NULL → unscoped legacy mode).

func (*WorkspaceStore) RepoByName

func (s *WorkspaceStore) RepoByName(ctx context.Context, name string) (*db.Repo, error)

RepoByName returns a single repo by unique name, or workspace.ErrNotFound.

func (*WorkspaceStore) UpsertModelPreference

func (s *WorkspaceStore) UpsertModelPreference(ctx context.Context, model string) error

UpsertModelPreference stores the workspace's model_preference. Returns workspace.ErrInvalidModel when model is not allowed.

func (*WorkspaceStore) UpsertRepo

UpsertRepo creates or updates a repo entry. path/description/language/ current_branch/known_issues/next_planned_step are presence-aware (Ω6, 2026-08-20-mcp-surface-spec.md): the ON CONFLICT CASE branches check the bound PARAMETER (?4-?8), not excluded.<col> (which is never NULL — it's whatever the VALUES clause carried), so a nil pointer preserves the stored value instead of wiping it. This closes the PG/SQLite divergence (known_issues was already COALESCE-preserved on PG but unconditionally overwritten here).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL