Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: ci

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
test:
name: test (go ${{ matrix.go }} / ${{ matrix.os }})
strategy:
fail-fast: false
matrix:
go: ["1.23", "1.24"]
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: checkout
uses: actions/checkout@v4

- name: setup go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
check-latest: true

- name: go mod download
run: go mod download

- name: go vet
run: go vet ./...

- name: go build
run: go build ./...

- name: go test
run: go test -race -count=1 ./...
37 changes: 37 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Binaries
*.exe
*.exe~
*.dll
*.so
*.dylib
/bin/
/dist/

# Test, coverage, profiling
*.test
*.out
*.prof
coverage.txt
coverage.html
coverage.out

# Go workspace and toolchain caches
go.work
go.work.sum
.go-cache/

# Editor and IDE state
.idea/
.vscode/
*.swp
*.swo
*~
.DS_Store

# Environment / local config
.env
.env.local

# Local scratch
/tmp/
/scratch/
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,56 @@
# lore
Structured knowledge primitive for AI agents. Apache 2 OSS Go library. Pluggable storage, embedding, vector store. BYO ingestion, serving, UI.

A structured knowledge primitive for AI agents. Apache 2.0 OSS Go library.

Lore stores classified knowledge entries (decisions, principles, procedures,
references, explanations, observations, research, ideas) and the typed edges
that connect them, then serves them back to retrieval pipelines that combine
lexical and semantic ranking. It ships as a Go library, not a service: callers
compose it into their own MCP servers, HTTP services, ingestion pipelines, or
CLI tools.

The library is built around three pluggable interfaces (`Store`, `Embedder`,
`VectorStore`) plus a composing `Retriever` and an optional `Ingester`. Each
interface ships with an in-process reference implementation (modernc.org/sqlite,
BGE int8, sqlite-vec) so a single binary can run against a local SQLite file
out of the box. Swap any of the three for Postgres, a remote embedding API,
pgvector, or anything else by implementing the interface.

## Install

```
go get github.com/mathomhaus/lore@latest
```

Requires Go 1.23 or newer.

## Status: pre-v1.0

Lore is pre-v1.0. The exported surface is stable in shape but may change in
detail between minor versions. Pin to a version, read release notes before
upgrading, and expect occasional breakage on `main`.

## What lore is not

- Not a CLI binary. Not an MCP server. Not an HTTP server. Not a UI.
- Not a hosted service. Not multi-tenant. Not an LLM client.
- Not a replacement for a full retrieval-augmented-generation framework.

Lore is the substrate. Everything above is a consumer's choice.

## Attribution

Lore extracts and generalizes the storage, embedding, and retrieval primitives
originally built inside [`mathomhaus/guild`](https://github.com/mathomhaus/guild).
Guild remains the opinionated agent-coordination platform that adds
quest, oath, and brief on top of these primitives.

## Spec

The architectural rationale and product positioning that informs this library
lives in the maintainer's notes at
`~/Library/CloudStorage/SynologyDrive-Obsidian/Personal/01 Projects/Agent Guild/Positioning/lore-product-mvp-2026-04-27.md`.

## License

Apache License 2.0. See [LICENSE](./LICENSE).
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/mathomhaus/lore

go 1.23
56 changes: 56 additions & 0 deletions pkg/lore/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Package lore is a structured knowledge primitive for AI agents.
//
// Lore stores classified knowledge entries (decisions, principles, procedures,
// references, explanations, observations, research, ideas) and the edges that
// connect them, then serves them back to retrieval pipelines that combine
// lexical and semantic ranking. It ships as a Go library, not a service:
// callers compose it into their own MCP servers, HTTP services, ingestion
// pipelines, or CLI tools.
//
// # Scope
//
// Lore v0.1.1 is library-only. It does not ship a CLI binary, an MCP server,
// an HTTP server, or a UI. It does not embed an LLM or talk to a remote
// service. It is the substrate; consumers compose the rest.
//
// # The three pluggable interfaces
//
// Lore is built around three swap points so users can mix in-process reference
// implementations with their own backends without rewriting retrieval logic:
//
// - Store persists entries and edges. The reference implementation lives in
// pkg/lore/store/sqlite and uses modernc.org/sqlite (pure Go) plus FTS5 for
// lexical search. Replace it with Postgres, MySQL, or any other engine by
// implementing the interface in pkg/lore/store.
//
// - Embedder turns text into vectors. The reference implementation lives in
// pkg/lore/embed/bge and runs an int8-quantized BGE model in process.
// Replace it with a remote embedding API or a different local model by
// implementing the interface in pkg/lore/embed.
//
// - VectorStore persists and queries vectors. The reference implementation
// lives in pkg/lore/vector/sqlitevec and uses the sqlite-vec extension.
// Replace it with pgvector, Qdrant, Weaviate, or any other engine by
// implementing the interface in pkg/lore/vector.
//
// On top of these three, lore composes a Retriever that runs lexical and
// vector queries in parallel and fuses results with reciprocal-rank fusion,
// and an optional Ingester that walks document trees and classifies chunks
// into entries on Path B (document ingestion). Path A (agent inscribe) goes
// straight through Store and Embedder without an ingest pipeline.
//
// # Caller-owned dependencies
//
// Constructors in this library accept already-initialized resources:
// *sql.DB, *http.Client, *slog.Logger, OpenTelemetry providers, and so on.
// The library does not open database connections, parse URLs, or read
// environment variables on the caller's behalf. This keeps the library
// stateless beyond its injected dependencies and safe to deploy across
// multiple replicas.
//
// # Stability
//
// Lore is pre-v1.0. The exported surface is stable in shape but may change in
// detail between minor versions. Pin to a version and read release notes
// before upgrading.
package lore
40 changes: 40 additions & 0 deletions pkg/lore/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package lore

import "errors"

// Sentinel errors are the canonical machine-readable failure modes. Callers
// match them with errors.Is. Implementations that wrap these with
// fmt.Errorf("...: %w", err) preserve the chain.
var (
// ErrNotFound indicates a requested entry, edge, or row does not exist.
ErrNotFound = errors.New("lore: not found")

// ErrDuplicate indicates a write would create a duplicate of an existing
// row keyed by a uniqueness constraint (for example same-title same-kind
// inside a single project, depending on the Store implementation).
ErrDuplicate = errors.New("lore: duplicate")

// ErrInvalidKind indicates a Kind value is outside the canonical
// taxonomy. Returned by Kind.Validate and by write paths that classify
// entries.
ErrInvalidKind = errors.New("lore: invalid kind")

// ErrInvalidArgument indicates a caller-supplied input failed validation
// (empty title, malformed source, negative limit, and so on). Callers
// should fix the input rather than retry.
ErrInvalidArgument = errors.New("lore: invalid argument")

// ErrConflict indicates an optimistic-concurrency check failed: the
// underlying row changed between read and write. Callers may retry after
// re-reading current state.
ErrConflict = errors.New("lore: conflict")

// ErrUnsupported indicates the requested operation is not implemented by
// the active backend (for example a vector query against a Store that
// only implements lexical retrieval).
ErrUnsupported = errors.New("lore: unsupported")

// ErrClosed indicates the component has been closed and rejects further
// I/O. Callers must construct a new instance to continue.
ErrClosed = errors.New("lore: closed")
)
41 changes: 41 additions & 0 deletions pkg/lore/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package lore

import (
"errors"
"fmt"
"testing"
)

// Sentinel errors must remain distinct so callers can pattern-match on them.
func TestSentinelErrors_Distinct(t *testing.T) {
all := []error{
ErrNotFound,
ErrDuplicate,
ErrInvalidKind,
ErrInvalidArgument,
ErrConflict,
ErrUnsupported,
ErrClosed,
}
for i, a := range all {
for j, b := range all {
if i == j {
continue
}
if errors.Is(a, b) {
t.Errorf("sentinels %d and %d collide: %v == %v", i, j, a, b)
}
}
}
}

// Wrapping with fmt.Errorf("%w") must preserve errors.Is matching.
func TestSentinelErrors_Wrap(t *testing.T) {
wrapped := fmt.Errorf("read entry 42: %w", ErrNotFound)
if !errors.Is(wrapped, ErrNotFound) {
t.Fatalf("wrapped ErrNotFound did not match via errors.Is")
}
if errors.Is(wrapped, ErrDuplicate) {
t.Fatalf("wrapped ErrNotFound matched unrelated sentinel ErrDuplicate")
}
}
91 changes: 91 additions & 0 deletions pkg/lore/kind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package lore

import "fmt"

// Kind classifies a lore entry. The canonical taxonomy has eight values that
// together cover both document-derived knowledge (procedure, reference,
// explanation) and agent-derived knowledge (decision, principle, observation,
// research, idea). Diátaxis prior art motivates the document-derived three;
// the rest carry classifications agents naturally produce while reasoning.
//
// Callers should treat Kind as an opaque typed string and use the constants
// declared below. New kinds may be added in future versions; consumers that
// switch on Kind should always handle an unknown value gracefully.
type Kind string

// Canonical kinds. Lore validates these on every write path and rejects any
// other value with ErrInvalidKind.
const (
// KindDecision records a choice made with rationale. ADRs, "we chose X
// because Y", policy decisions.
KindDecision Kind = "decision"

// KindPrinciple records a durable rule or invariant. "Always X", "never Y",
// coding standards.
KindPrinciple Kind = "principle"

// KindProcedure records a step-by-step how-to. Runbooks, deploy guides,
// incident response playbooks.
KindProcedure Kind = "procedure"

// KindReference records a fact to look up. Service catalogs, API specs,
// configuration tables, glossaries.
KindReference Kind = "reference"

// KindExplanation records a concept or mental model. Architecture
// overviews, "how auth works", design walkthroughs.
KindExplanation Kind = "explanation"

// KindObservation records an empirical finding. "We measured X", "we saw
// Y", postmortems.
KindObservation Kind = "observation"

// KindResearch records the result of an investigation. Spike outputs,
// "we explored X, here's what we found".
KindResearch Kind = "research"

// KindIdea records a proposal not yet decided. Design sketches, open
// questions, "what if we did X".
KindIdea Kind = "idea"
)

// AllKinds returns the canonical kinds in display order. The order is stable
// and may be relied upon by UIs, tests, and migration tooling.
func AllKinds() []Kind {
return []Kind{
KindDecision,
KindPrinciple,
KindProcedure,
KindReference,
KindExplanation,
KindObservation,
KindResearch,
KindIdea,
}
}

// Validate reports whether k is one of the canonical kinds. It returns
// ErrInvalidKind wrapped with the offending value when validation fails so
// callers can inspect both the sentinel and the bad input.
func (k Kind) Validate() error {
switch k {
case KindDecision,
KindPrinciple,
KindProcedure,
KindReference,
KindExplanation,
KindObservation,
KindResearch,
KindIdea:
return nil
default:
return fmt.Errorf("kind %q: %w", string(k), ErrInvalidKind)
}
}

// String returns the wire-format string for k. It is the identity function on
// the underlying string and is provided for symmetry with fmt.Stringer
// expectations.
func (k Kind) String() string {
return string(k)
}
Loading
Loading