index

package
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: May 31, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package index implements the SQLite FTS5 shadow index that memory.fetch_context searches and propose_update updates incrementally.

Production version of spike S4 (spikes/s4-fts5-incremental). The two PRAGMAs identified there (journal_mode=WAL, synchronous=NORMAL) are non-optional: without them per-COMMIT fsync dominates and per-update cost blows past the <10ms budget.

See docs/patterns/sqlite-fts5-shadow-index.md and design doc v0.4.1 §20.

Index

Constants

View Source
const (
	// ScopeBoost is applied to results whose file matches any of the
	// caller-supplied scope strings (substring match).
	ScopeBoost = 2.0

	// FreshBoost is applied to results in files marked fresh in the
	// FreshFiles map of RankingContext.
	FreshBoost = 1.5

	// ArchivePenalty is applied to results whose file lives under the
	// archive path prefix.
	ArchivePenalty = 0.4

	// StalePenalty is applied to results in files marked stale.
	StalePenalty = 0.6

	// ActiveBranchBoost is applied to results whose section content
	// references the active branch name (e.g., a decision scoped to a
	// feature branch). Suppressed for generic integration branches.
	ActiveBranchBoost = 1.3

	// ChangedRefBoost is applied to decisions/pitfalls whose content
	// references a file with uncommitted changes — surface the prior art
	// for whatever you're touching right now.
	ChangedRefBoost = 1.4

	// LowConfidencePenalty is applied to sections that declare a low-trust
	// confidence (inferred / stale / unknown).
	LowConfidencePenalty = 0.8
)

Multiplier constants from design doc v0.4.1 §20.4. BM25 scores in SQLite FTS5 are NEGATIVE (more negative = better match), so multiplication acts in the natural direction:

  • boost (multiplier > 1) → score becomes MORE negative → ranks higher
  • penalty (multiplier < 1) → score becomes LESS negative → ranks lower

Example: a result with raw BM25 score -10.0

×2.0 boost   → -20.0 (better)
×0.4 penalty → -4.0  (worse)
View Source
const Schema = `` /* 864-byte string literal not displayed */

Schema mirrors design doc v0.4.1 §20.2. Three tables:

memory_search   — FTS5 virtual table; the search hot path.
memory_sections — byte offsets + content hash per (file, section_id).
                  The fetch path reads this to splice section bytes out
                  of the source Markdown file.
memory_docs     — per-file metadata for ranking signals (freshness,
                  archived, local_state, etc.).
View Source
const SchemaVersion = 1

SchemaVersion is the value Init stores in PRAGMA user_version. Bumping it signals a schema migration; the migrator (deferred to v0.5) compares the stored value against this constant on open.

Variables

View Source
var ErrNotFound = errors.New("index: row not found")

ErrNotFound is returned when GetSection / GetFile can't find the row.

Functions

This section is empty.

Types

type FileDoc

type FileDoc struct {
	File         string
	Category     string
	Freshness    string
	Confidence   string
	LastModified string // RFC3339; empty if unknown
	Committed    bool
	LocalState   bool
	Archived     bool
	SizeBytes    int
	Checksum     string
}

FileDoc bundles per-file metadata for ranking + status reporting.

type Index

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

Index is an open SQLite shadow index. Not safe for concurrent calls from multiple goroutines (the underlying *sql.DB is configured with SetMaxOpenConns(1) to match the single-writer concurrency model in v0.4.1 §11).

func Open

func Open(path string) (*Index, error)

Open opens (or creates) the index at path. WAL + synchronous=NORMAL are applied via the connection URI so every connection database/sql hands out has them set.

func (*Index) Close

func (i *Index) Close() error

Close closes the underlying database. Idempotent — calling on a half-closed Index returns nil.

func (*Index) CountFiles

func (i *Index) CountFiles(ctx context.Context) (int, error)

CountFiles returns the total number of rows in memory_docs.

func (*Index) CountSections

func (i *Index) CountSections(ctx context.Context) (int, error)

CountSections returns the total number of rows in memory_sections. Used by tests and status reporting.

func (*Index) DeleteFile

func (i *Index) DeleteFile(ctx context.Context, file string) error

DeleteFile removes ALL rows for the given file across the three tables. Used when a memory file is removed from the layout (e.g., archive flow).

func (*Index) DeleteSections

func (i *Index) DeleteSections(ctx context.Context, file string, sectionIDs []string) error

DeleteSections removes the listed (file, section_id) rows from both memory_search and memory_sections in a single transaction. An empty sectionIDs slice is a no-op.

func (*Index) GetFile

func (i *Index) GetFile(ctx context.Context, file string) (FileDoc, error)

GetFile returns the memory_docs row for file. Returns ErrNotFound when missing.

func (*Index) GetSection

func (i *Index) GetSection(ctx context.Context, file, sectionID string) (SectionInfo, error)

GetSection returns the memory_sections row for a (file, section_id) pair. Returns ErrNotFound if the row doesn't exist.

func (*Index) IndexFile

func (i *Index) IndexFile(ctx context.Context, memDir, relPath string, cat schema.Category, opts RebuildOpts) error

IndexFile reads, optionally assigns missing anchor IDs, parses, and indexes a single file. The full path on disk is memDir/relPath (relPath in forward-slash form).

If opts.AssignMissingIDs is true AND the category requires section IDs, AssignMissingIDs runs first; modified bytes are written back atomically before parsing. Files that don't need assignments are not touched.

func (*Index) Init

func (i *Index) Init(ctx context.Context) error

Init applies the schema (idempotent via IF NOT EXISTS) and writes the SchemaVersion to PRAGMA user_version.

func (*Index) ListFiles

func (i *Index) ListFiles(ctx context.Context, categoryFilter string) ([]FileDoc, error)

ListFiles returns memory_docs entries; if categoryFilter is non-empty, only rows whose category matches are returned.

func (*Index) ListSections

func (i *Index) ListSections(ctx context.Context, file string) ([]SectionInfo, error)

ListSections returns every section for a file in document order (ascending byte_start).

func (*Index) Path

func (i *Index) Path() string

Path returns the on-disk path of the index file.

func (*Index) RebuildAll

func (i *Index) RebuildAll(ctx context.Context, memDir string, sch *schema.Schema, opts RebuildOpts) error

RebuildAll wipes the index and re-walks memDir, indexing every Markdown file that maps to a schema category. memDir is the absolute path to .agent-memory/.

The two pre-existing rows for any file present in the wipe survive — we fully reset the three index tables and rebuild from disk. This is the canonical "make everything consistent" operation; M3's incremental path (per-file upserts after propose_update) keeps the index fresh between rebuilds.

func (*Index) Search

func (i *Index) Search(ctx context.Context, query string, limit int) ([]SearchResult, error)

Search runs an FTS5 MATCH query and returns hits sorted by ascending BM25 score (FTS5 convention: lower = better). limit ≤ 0 falls back to 50.

The raw query is treated as natural language, NOT as FTS5 query syntax: sanitizeFTSMatch tokenizes it and quotes each term, so punctuation, hyphens (`auto-apply`), and reserved words (AND/OR/NEAR) are matched literally instead of crashing the parser with a syntax/"no such column" error. Terms are OR-ed (match any) and ranked by BM25, so a multi-word natural-language query retrieves the best partial matches. A query with no alphanumeric tokens returns no results without error — callers (the fetch pipeline) handle the empty case by returning the bootstrap pack.

func (*Index) UpsertFile

func (i *Index) UpsertFile(ctx context.Context, doc FileDoc) error

UpsertFile inserts or updates the memory_docs row for file.

func (*Index) UpsertSections

func (i *Index) UpsertSections(ctx context.Context, sections []SectionDoc) error

UpsertSections replaces (file, section_id) rows for every entry in `sections`. Each row is handled in one DB transaction:

DELETE FROM memory_search WHERE file=? AND section_id=?
INSERT INTO memory_search (...) VALUES (...)
INSERT INTO memory_sections (...) VALUES (...) ON CONFLICT DO UPDATE

FTS5 has no ON CONFLICT, so memory_search uses DELETE-then-INSERT. memory_sections uses standard UPSERT.

An empty slice is a no-op (no transaction opened).

func (*Index) Version

func (i *Index) Version(ctx context.Context) (int, error)

Version returns the stored PRAGMA user_version. Used by doctor and by the future M3+ migration check.

type RankingContext

type RankingContext struct {
	// Scope: each entry is checked as a substring of the result's file
	// path. A hit applies ScopeBoost. Multiple scope entries → boost
	// applies if ANY matches (boost is applied at most once per result).
	Scope []string

	// ArchivePathPrefix marks the archive directory. Defaults to
	// "archive/" if empty. Results whose File starts with this prefix
	// receive ArchivePenalty.
	ArchivePathPrefix string

	// FreshFiles → results in these files get FreshBoost. Lookup map.
	FreshFiles map[string]bool

	// StaleFiles → results in these files get StalePenalty. Lookup map.
	StaleFiles map[string]bool

	// ActiveBranch is the current git branch name. A result whose section
	// content references it earns ActiveBranchBoost. Empty / generic
	// integration branches (main, master, …) apply no boost.
	ActiveBranch string

	// ChangedFiles are repo-relative paths with uncommitted changes. A
	// decision/pitfall section referencing any of them earns ChangedRefBoost.
	ChangedFiles []string
}

RankingContext bundles the inputs ApplyRankingSignals needs. All fields are optional; an empty RankingContext leaves BM25 ordering unchanged.

type RebuildOpts

type RebuildOpts struct {
	// AssignMissingIDs runs internal/markdown.AssignMissingIDs on every
	// file whose category has SectionIDRequired=true before indexing.
	// Mutates the file on disk via atomic write; only modified files are
	// touched. Idempotent — running rebuild twice produces the same
	// on-disk state.
	AssignMissingIDs bool
}

RebuildOpts configures RebuildAll.

type SearchResult

type SearchResult struct {
	File      string
	SectionID string
	Title     string
	Headings  string
	Snippet   string  // FTS5 snippet() output, marked with [..]
	Score     float64 // bm25() — lower is better in FTS5
	Content   string  // full indexed section body; feeds content-based ranking signals
}

SearchResult is one hit from a Search query, ranked by BM25.

func ApplyRankingSignals

func ApplyRankingSignals(results []SearchResult, rctx RankingContext) []SearchResult

ApplyRankingSignals modifies the Score field of each result per the multipliers documented in design doc §20.4, then sorts results by ascending Score (FTS5 convention: lower = better). Returns the same slice for caller convenience.

File-level signals (scope, freshness, archive, stale) key off the result's path; content-level signals (active-branch reference, changed-file reference, low confidence) inspect the indexed section body (SearchResult.Content).

Multipliers compose: a result that's in scope AND stale gets score × ScopeBoost × StalePenalty.

The order in which multipliers are applied does not affect the final score (multiplication is commutative); the iteration order in this implementation is fixed for predictability when debugging.

type SectionDoc

type SectionDoc struct {
	File         string
	SectionID    string
	Heading      string
	HeadingLevel int
	Title        string // FTS5: typically equals Heading
	Headings     string // FTS5: breadcrumb of ancestor headings (future use)
	Content      string // FTS5: section body
	Tags         string // FTS5: space-separated tag tokens (future use)
	ByteStart    int
	ByteEnd      int
	ContentHash  string
}

SectionDoc bundles the per-section data the index needs. Field names mirror the FTS5 + memory_sections columns; the index doesn't try to validate them further (e.g., that ByteEnd > ByteStart) — that's the caller's job.

type SectionInfo

type SectionInfo struct {
	File         string
	SectionID    string
	Heading      string
	HeadingLevel int
	ByteStart    int
	ByteEnd      int
	ContentHash  string
}

SectionInfo mirrors a memory_sections row. Returned by GetSection / ListSections; used by the fetch path to splice section bytes out of the source Markdown file.

Jump to

Keyboard shortcuts

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