feat(repos): add kb.yaml knowledge base config
Some checks failed
Security Scan / security (push) Failing after 8s
Test / test (push) Failing after 1m14s

KBConfig (.core/kb.yaml, checked in):
- Wiki mirror: enabled flag, local dir, remote SSH base URL
- Search: Qdrant host/port, collection (openbrain), Ollama URL,
  embed model (embeddinggemma), default top_k
- Helpers: WikiRepoURL(name), WikiLocalPath(root, name)

Data already lives in Qdrant (homelab-embedded) — this just
configures the search client and local wiki clone paths.

Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-03-06 15:55:48 +00:00
parent f751b29170
commit e4342454b8
2 changed files with 223 additions and 0 deletions

116
repos/kbconfig.go Normal file
View file

@ -0,0 +1,116 @@
package repos
import (
"fmt"
"path/filepath"
"forge.lthn.ai/core/go-io"
"gopkg.in/yaml.v3"
)
// KBConfig holds knowledge base configuration for a workspace.
// Stored at .core/kb.yaml and checked into git.
type KBConfig struct {
Version int `yaml:"version"`
Wiki WikiConfig `yaml:"wiki"`
Search KBSearch `yaml:"search"`
}
// WikiConfig controls local wiki mirror behaviour.
type WikiConfig struct {
// Enabled toggles wiki cloning on sync.
Enabled bool `yaml:"enabled"`
// Dir is the local directory for wiki clones, relative to .core/.
Dir string `yaml:"dir"`
// Remote is the SSH base URL for wiki repos (e.g. ssh://git@forge.lthn.ai:2223/core).
// Repo wikis are at {Remote}/{name}.wiki.git
Remote string `yaml:"remote"`
}
// KBSearch configures vector search against the OpenBrain Qdrant collection.
type KBSearch struct {
// QdrantHost is the Qdrant server (gRPC).
QdrantHost string `yaml:"qdrant_host"`
// QdrantPort is the gRPC port.
QdrantPort int `yaml:"qdrant_port"`
// Collection is the Qdrant collection name.
Collection string `yaml:"collection"`
// OllamaURL is the Ollama API base URL for embedding queries.
OllamaURL string `yaml:"ollama_url"`
// EmbedModel is the Ollama model for embedding.
EmbedModel string `yaml:"embed_model"`
// TopK is the default number of results.
TopK int `yaml:"top_k"`
}
// DefaultKBConfig returns sensible defaults for knowledge base config.
func DefaultKBConfig() *KBConfig {
return &KBConfig{
Version: 1,
Wiki: WikiConfig{
Enabled: true,
Dir: "kb/wiki",
Remote: "ssh://git@forge.lthn.ai:2223/core",
},
Search: KBSearch{
QdrantHost: "qdrant.lthn.sh",
QdrantPort: 6334,
Collection: "openbrain",
OllamaURL: "https://ollama.lthn.sh",
EmbedModel: "embeddinggemma",
TopK: 5,
},
}
}
// LoadKBConfig reads .core/kb.yaml from the given workspace root directory.
// Returns defaults if the file does not exist.
func LoadKBConfig(m io.Medium, root string) (*KBConfig, error) {
path := filepath.Join(root, ".core", "kb.yaml")
if !m.Exists(path) {
return DefaultKBConfig(), nil
}
content, err := m.Read(path)
if err != nil {
return nil, fmt.Errorf("failed to read kb config: %w", err)
}
kb := DefaultKBConfig()
if err := yaml.Unmarshal([]byte(content), kb); err != nil {
return nil, fmt.Errorf("failed to parse kb config: %w", err)
}
return kb, nil
}
// SaveKBConfig writes .core/kb.yaml to the given workspace root directory.
func SaveKBConfig(m io.Medium, root string, kb *KBConfig) error {
coreDir := filepath.Join(root, ".core")
if err := m.EnsureDir(coreDir); err != nil {
return fmt.Errorf("failed to create .core directory: %w", err)
}
data, err := yaml.Marshal(kb)
if err != nil {
return fmt.Errorf("failed to marshal kb config: %w", err)
}
path := filepath.Join(coreDir, "kb.yaml")
if err := m.Write(path, string(data)); err != nil {
return fmt.Errorf("failed to write kb config: %w", err)
}
return nil
}
// WikiRepoURL returns the full clone URL for a repo's wiki.
func (kb *KBConfig) WikiRepoURL(repoName string) string {
return fmt.Sprintf("%s/%s.wiki.git", kb.Wiki.Remote, repoName)
}
// WikiLocalPath returns the local path for a repo's wiki clone.
func (kb *KBConfig) WikiLocalPath(root, repoName string) string {
return filepath.Join(root, ".core", kb.Wiki.Dir, repoName)
}

107
repos/kbconfig_test.go Normal file
View file

@ -0,0 +1,107 @@
package repos
import (
"testing"
"forge.lthn.ai/core/go-io"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ── DefaultKBConfig ────────────────────────────────────────────────
func TestDefaultKBConfig_Good(t *testing.T) {
kb := DefaultKBConfig()
assert.Equal(t, 1, kb.Version)
assert.True(t, kb.Wiki.Enabled)
assert.Equal(t, "kb/wiki", kb.Wiki.Dir)
assert.Contains(t, kb.Wiki.Remote, "forge.lthn.ai")
assert.Equal(t, "qdrant.lthn.sh", kb.Search.QdrantHost)
assert.Equal(t, 6334, kb.Search.QdrantPort)
assert.Equal(t, "openbrain", kb.Search.Collection)
assert.Equal(t, "embeddinggemma", kb.Search.EmbedModel)
assert.Equal(t, 5, kb.Search.TopK)
}
// ── Load / Save round-trip ─────────────────────────────────────────
func TestKBConfig_LoadSave_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.EnsureDir("/workspace/.core")
kb := DefaultKBConfig()
kb.Search.TopK = 10
kb.Search.Collection = "custom_brain"
err := SaveKBConfig(m, "/workspace", kb)
require.NoError(t, err)
loaded, err := LoadKBConfig(m, "/workspace")
require.NoError(t, err)
assert.Equal(t, 1, loaded.Version)
assert.Equal(t, 10, loaded.Search.TopK)
assert.Equal(t, "custom_brain", loaded.Search.Collection)
assert.True(t, loaded.Wiki.Enabled)
}
func TestKBConfig_Load_Good_NoFile(t *testing.T) {
m := io.NewMockMedium()
_ = m.EnsureDir("/workspace/.core")
kb, err := LoadKBConfig(m, "/workspace")
require.NoError(t, err)
assert.Equal(t, DefaultKBConfig().Search.Collection, kb.Search.Collection)
}
func TestKBConfig_Load_Good_PartialOverride(t *testing.T) {
m := io.NewMockMedium()
_ = m.Write("/workspace/.core/kb.yaml", `
version: 1
search:
top_k: 20
collection: my_brain
`)
kb, err := LoadKBConfig(m, "/workspace")
require.NoError(t, err)
assert.Equal(t, 20, kb.Search.TopK)
assert.Equal(t, "my_brain", kb.Search.Collection)
// Defaults preserved
assert.True(t, kb.Wiki.Enabled)
assert.Equal(t, "embeddinggemma", kb.Search.EmbedModel)
}
func TestKBConfig_Load_Bad_InvalidYAML(t *testing.T) {
m := io.NewMockMedium()
_ = m.Write("/workspace/.core/kb.yaml", "{{{{broken")
_, err := LoadKBConfig(m, "/workspace")
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse")
}
// ── WikiRepoURL ────────────────────────────────────────────────────
func TestKBConfig_WikiRepoURL_Good(t *testing.T) {
kb := DefaultKBConfig()
url := kb.WikiRepoURL("go-scm")
assert.Equal(t, "ssh://git@forge.lthn.ai:2223/core/go-scm.wiki.git", url)
}
func TestKBConfig_WikiRepoURL_Good_CustomRemote(t *testing.T) {
kb := &KBConfig{
Wiki: WikiConfig{Remote: "ssh://git@git.example.com/org"},
}
url := kb.WikiRepoURL("my-repo")
assert.Equal(t, "ssh://git@git.example.com/org/my-repo.wiki.git", url)
}
// ── WikiLocalPath ──────────────────────────────────────────────────
func TestKBConfig_WikiLocalPath_Good(t *testing.T) {
kb := DefaultKBConfig()
path := kb.WikiLocalPath("/workspace", "go-scm")
assert.Equal(t, "/workspace/.core/kb/wiki/go-scm", path)
}