Release dec 2025 (#15)

* Resolves issue #13

- Switched model to bge-small-en-v1.5
- Added lazy re-embedding
- Added model version tracking per vector
- Added conversion of vectors to the new model

* Add lfs support to the workflow.

* Implements importance scoring with decay + voting #6

* Resolves issue #5 by marking observations as superseeded and scheduled for deletion

* Implement pattern detection #7

* Improve injections and observations accuracy

- Session start: Recent observations for project context (recency-based)
- User prompt: Semantically relevant observations (similarity-based with threshold)

* Added two stage retrieval with bi and cross encoder #8

* Implement query expansion and reformulation #9

* Knowledge graph and relationships ( resolves #4 )

- File Overlap Detection: Detects relationships when observations modify/read the same files
- Concept Overlap Detection: Detects relationships based on shared semantic concepts
- Type Progression Detection: Infers relationships from natural observation type progressions (e.g., discovery → bugfix = "fixes")
- Temporal Proximity Detection: Detects relationships between observations in the same session within 5 minutes
- Narrative Mention Detection: Detects explicit relationship language in narratives (e.g., "fixes", "depends on", "supersedes")

* Add visualisation of the relations to the dashboard.

* fixup! Add visualisation of the relations to the dashboard.

* Update documentation with new settings and screenshots.
This commit is contained in:
2025-12-19 17:57:11 +00:00
committed by GitHub
parent 48957a6c81
commit f79782a008
69 changed files with 43967 additions and 194 deletions
+448
View File
@@ -0,0 +1,448 @@
// Package expansion provides context-aware query expansion for improved search recall.
package expansion
import (
"context"
"regexp"
"strings"
"sync"
"github.com/lukaszraczylo/claude-mnemonic/internal/embedding"
"github.com/rs/zerolog/log"
)
// QueryIntent represents the detected intent of a query.
type QueryIntent string
const (
// IntentQuestion indicates a question-type query (how, why, what, etc.)
IntentQuestion QueryIntent = "question"
// IntentError indicates an error/debugging query
IntentError QueryIntent = "error"
// IntentImplementation indicates an implementation/coding query
IntentImplementation QueryIntent = "implementation"
// IntentArchitecture indicates an architecture/design query
IntentArchitecture QueryIntent = "architecture"
// IntentGeneral indicates a general lookup query
IntentGeneral QueryIntent = "general"
)
// ExpandedQuery represents a query variant with metadata.
type ExpandedQuery struct {
Query string `json:"query"`
Weight float64 `json:"weight"` // Weight for result merging (0.0-1.0)
Source string `json:"source"` // Where this expansion came from
Intent QueryIntent `json:"intent"` // Detected intent
}
// Expander provides context-aware query expansion.
type Expander struct {
embedSvc *embedding.Service
vocabulary []VocabEntry // Known vocabulary from observations
vocabVectors [][]float32 // Embeddings for vocabulary entries
vocabMu sync.RWMutex // Protects vocabulary
intentPatterns map[QueryIntent][]*regexp.Regexp
}
// VocabEntry represents a vocabulary term from observations.
type VocabEntry struct {
Term string // The term itself
Weight float64 // How common/important this term is (0.0-1.0)
Source string // Where it came from (title, concept, narrative)
}
// Config holds expander configuration.
type Config struct {
// MaxExpansions limits the number of expanded queries returned
MaxExpansions int
// MinSimilarity is the minimum similarity score for vocabulary expansion
MinSimilarity float64
// EnableVocabularyExpansion enables finding related terms from observations
EnableVocabularyExpansion bool
}
// DefaultConfig returns sensible default configuration.
func DefaultConfig() Config {
return Config{
MaxExpansions: 4,
MinSimilarity: 0.5,
EnableVocabularyExpansion: true,
}
}
// NewExpander creates a new query expander.
func NewExpander(embedSvc *embedding.Service) *Expander {
e := &Expander{
embedSvc: embedSvc,
intentPatterns: buildIntentPatterns(),
}
return e
}
// buildIntentPatterns creates regex patterns for intent detection.
func buildIntentPatterns() map[QueryIntent][]*regexp.Regexp {
patterns := make(map[QueryIntent][]*regexp.Regexp)
// Question patterns
patterns[IntentQuestion] = []*regexp.Regexp{
regexp.MustCompile(`(?i)^(how|why|what|when|where|which|who)\b`),
regexp.MustCompile(`(?i)\?$`),
regexp.MustCompile(`(?i)\b(explain|describe|understand)\b`),
}
// Error/debugging patterns
patterns[IntentError] = []*regexp.Regexp{
regexp.MustCompile(`(?i)\b(error|bug|issue|problem|fail|crash|exception|panic)\b`),
regexp.MustCompile(`(?i)\b(fix|debug|troubleshoot|resolve)\b`),
regexp.MustCompile(`(?i)\b(doesn't work|not working|broken)\b`),
}
// Implementation patterns
patterns[IntentImplementation] = []*regexp.Regexp{
regexp.MustCompile(`(?i)\b(implement|add|create|build|write|code)\b`),
regexp.MustCompile(`(?i)\b(function|method|handler|endpoint|api)\b`),
regexp.MustCompile(`(?i)\b(feature|functionality)\b`),
}
// Architecture patterns
patterns[IntentArchitecture] = []*regexp.Regexp{
regexp.MustCompile(`(?i)\b(architecture|design|pattern|structure)\b`),
regexp.MustCompile(`(?i)\b(component|module|layer|service)\b`),
regexp.MustCompile(`(?i)\b(flow|pipeline|workflow)\b`),
}
return patterns
}
// DetectIntent analyzes a query to determine its intent.
func (e *Expander) DetectIntent(query string) QueryIntent {
query = strings.TrimSpace(query)
if query == "" {
return IntentGeneral
}
// Check patterns in priority order
intentOrder := []QueryIntent{IntentError, IntentQuestion, IntentImplementation, IntentArchitecture}
for _, intent := range intentOrder {
patterns := e.intentPatterns[intent]
for _, pattern := range patterns {
if pattern.MatchString(query) {
return intent
}
}
}
return IntentGeneral
}
// Expand generates expanded query variants based on the original query.
func (e *Expander) Expand(ctx context.Context, query string, cfg Config) []ExpandedQuery {
query = strings.TrimSpace(query)
if query == "" {
return nil
}
intent := e.DetectIntent(query)
expansions := make([]ExpandedQuery, 0, cfg.MaxExpansions)
// Always include the original query with highest weight
expansions = append(expansions, ExpandedQuery{
Query: query,
Weight: 1.0,
Source: "original",
Intent: intent,
})
// Generate intent-based expansions
intentExpansions := e.expandByIntent(query, intent)
expansions = append(expansions, intentExpansions...)
// Generate vocabulary-based expansions if enabled and we have vocabulary
if cfg.EnableVocabularyExpansion && e.embedSvc != nil && len(e.vocabulary) > 0 {
vocabExpansions := e.expandByVocabulary(ctx, query, cfg.MinSimilarity)
expansions = append(expansions, vocabExpansions...)
}
// Deduplicate and limit
expansions = deduplicateExpansions(expansions)
if len(expansions) > cfg.MaxExpansions {
expansions = expansions[:cfg.MaxExpansions]
}
log.Debug().
Str("query", truncate(query, 50)).
Str("intent", string(intent)).
Int("expansions", len(expansions)).
Msg("Query expanded")
return expansions
}
// expandByIntent generates expansions based on detected query intent.
func (e *Expander) expandByIntent(query string, intent QueryIntent) []ExpandedQuery {
var expansions []ExpandedQuery
// Extract key terms from query for context-aware expansion
keyTerms := extractKeyTerms(query)
switch intent {
case IntentQuestion:
// For questions, create a declarative variant
declarative := makeDeclarative(query)
if declarative != query {
expansions = append(expansions, ExpandedQuery{
Query: declarative,
Weight: 0.85,
Source: "intent:declarative",
Intent: intent,
})
}
case IntentError:
// For errors, expand with solution-oriented terms
if len(keyTerms) > 0 {
solutionQuery := strings.Join(keyTerms, " ") + " solution fix"
expansions = append(expansions, ExpandedQuery{
Query: solutionQuery,
Weight: 0.8,
Source: "intent:solution",
Intent: intent,
})
}
case IntentImplementation:
// For implementation queries, focus on the what/how
if len(keyTerms) > 0 {
howQuery := "how " + strings.Join(keyTerms, " ")
expansions = append(expansions, ExpandedQuery{
Query: howQuery,
Weight: 0.75,
Source: "intent:how",
Intent: intent,
})
}
case IntentArchitecture:
// For architecture queries, expand with design context
if len(keyTerms) > 0 {
designQuery := strings.Join(keyTerms, " ") + " design structure"
expansions = append(expansions, ExpandedQuery{
Query: designQuery,
Weight: 0.75,
Source: "intent:design",
Intent: intent,
})
}
case IntentGeneral:
// For general queries, try noun phrase extraction
// No additional expansion - rely on vocabulary expansion
}
return expansions
}
// expandByVocabulary finds similar terms from the observation vocabulary.
func (e *Expander) expandByVocabulary(ctx context.Context, query string, minSimilarity float64) []ExpandedQuery {
e.vocabMu.RLock()
defer e.vocabMu.RUnlock()
if len(e.vocabulary) == 0 || e.embedSvc == nil {
return nil
}
// Embed the query
queryEmb, err := e.embedSvc.Embed(query)
if err != nil {
log.Warn().Err(err).Msg("Failed to embed query for vocabulary expansion")
return nil
}
// Find similar vocabulary terms
type scoredTerm struct {
entry VocabEntry
score float64
}
var similar []scoredTerm
for i, entry := range e.vocabulary {
if i >= len(e.vocabVectors) {
break
}
score := cosineSimilarity(queryEmb, e.vocabVectors[i])
if score >= minSimilarity {
similar = append(similar, scoredTerm{entry: entry, score: score})
}
}
if len(similar) == 0 {
return nil
}
// Sort by score (descending) using bubble sort
for i := 0; i < len(similar)-1; i++ {
for j := i + 1; j < len(similar); j++ {
if similar[j].score > similar[i].score {
similar[i], similar[j] = similar[j], similar[i]
}
}
}
// Create expansion by combining top similar terms with query
var expansions []ExpandedQuery
if len(similar) > 0 {
// Take top 2 similar terms and combine with original key terms
keyTerms := extractKeyTerms(query)
for i := 0; i < min(2, len(similar)); i++ {
term := similar[i].entry.Term
// Don't add if term is already in query
if strings.Contains(strings.ToLower(query), strings.ToLower(term)) {
continue
}
combinedQuery := strings.Join(keyTerms, " ") + " " + term
expansions = append(expansions, ExpandedQuery{
Query: combinedQuery,
Weight: 0.7 * similar[i].score * similar[i].entry.Weight,
Source: "vocabulary:" + term,
Intent: IntentGeneral,
})
}
}
return expansions
}
// Helper functions
// extractKeyTerms extracts meaningful terms from a query.
func extractKeyTerms(query string) []string {
// Common stop words to filter out
stopWords := map[string]bool{
"a": true, "an": true, "the": true, "is": true, "are": true,
"was": true, "were": true, "be": true, "been": true, "being": true,
"have": true, "has": true, "had": true, "do": true, "does": true,
"did": true, "will": true, "would": true, "could": true, "should": true,
"may": true, "might": true, "must": true, "can": true,
"i": true, "me": true, "my": true, "we": true, "our": true,
"you": true, "your": true, "it": true, "its": true,
"this": true, "that": true, "these": true, "those": true,
"what": true, "which": true, "who": true, "whom": true,
"how": true, "why": true, "when": true, "where": true,
"to": true, "for": true, "with": true, "about": true, "from": true,
"in": true, "on": true, "at": true, "by": true, "of": true,
"and": true, "or": true, "but": true, "if": true, "then": true,
}
// Split and filter
words := strings.Fields(strings.ToLower(query))
var terms []string
for _, word := range words {
// Remove punctuation
word = strings.Trim(word, ".,?!;:'\"()[]{}")
if len(word) < 2 {
continue
}
if stopWords[word] {
continue
}
terms = append(terms, word)
}
return terms
}
// makeDeclarative converts a question to a declarative statement.
func makeDeclarative(query string) string {
query = strings.TrimSpace(query)
// Remove question mark
query = strings.TrimSuffix(query, "?")
// Handle common question patterns
patterns := []struct {
prefix string
replacement string
}{
{"how do i ", ""},
{"how to ", ""},
{"how does ", ""},
{"how is ", ""},
{"what is ", ""},
{"what are ", ""},
{"why does ", ""},
{"why is ", ""},
{"where is ", ""},
{"where are ", ""},
{"when does ", ""},
{"when is ", ""},
}
lower := strings.ToLower(query)
for _, p := range patterns {
if strings.HasPrefix(lower, p.prefix) {
return strings.TrimSpace(query[len(p.prefix):])
}
}
return query
}
// deduplicateExpansions removes duplicate queries while preserving order.
func deduplicateExpansions(expansions []ExpandedQuery) []ExpandedQuery {
seen := make(map[string]bool)
result := make([]ExpandedQuery, 0, len(expansions))
for _, exp := range expansions {
normalized := strings.ToLower(strings.TrimSpace(exp.Query))
if !seen[normalized] {
seen[normalized] = true
result = append(result, exp)
}
}
return result
}
// cosineSimilarity computes cosine similarity between two vectors.
func cosineSimilarity(a, b []float32) float64 {
if len(a) != len(b) || len(a) == 0 {
return 0
}
var dot, normA, normB float64
for i := range a {
dot += float64(a[i]) * float64(b[i])
normA += float64(a[i]) * float64(a[i])
normB += float64(b[i]) * float64(b[i])
}
if normA == 0 || normB == 0 {
return 0
}
return dot / (sqrt(normA) * sqrt(normB))
}
// sqrt is a simple square root implementation.
func sqrt(x float64) float64 {
if x <= 0 {
return 0
}
z := x
for i := 0; i < 10; i++ {
z = (z + x/z) / 2
}
return z
}
// truncate truncates a string to maxLen characters.
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}
+518
View File
@@ -0,0 +1,518 @@
// Package expansion provides context-aware query expansion for improved search recall.
package expansion
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
// ExpanderSuite tests the Expander functionality.
type ExpanderSuite struct {
suite.Suite
expander *Expander
}
func TestExpanderSuite(t *testing.T) {
suite.Run(t, new(ExpanderSuite))
}
func (s *ExpanderSuite) SetupTest() {
// Create expander without embedding service for basic tests
s.expander = NewExpander(nil)
}
// TestNewExpander tests expander creation.
func (s *ExpanderSuite) TestNewExpander() {
e := NewExpander(nil)
s.NotNil(e)
s.NotNil(e.intentPatterns)
s.Nil(e.embedSvc)
}
// TestDetectIntent tests intent detection.
func (s *ExpanderSuite) TestDetectIntent() {
tests := []struct {
name string
query string
expected QueryIntent
}{
// Question intents
{"how_question", "how do I implement auth?", IntentQuestion},
{"why_question", "why does this fail?", IntentError}, // "fail" triggers error first
{"what_question", "what is the purpose of this function?", IntentQuestion},
{"question_mark", "the handler for auth?", IntentQuestion},
{"explain", "explain the architecture", IntentQuestion},
// Error intents
{"error_word", "authentication error in login", IntentError},
{"bug_word", "bug in user registration", IntentError},
{"fix_word", "fix the memory leak", IntentError},
{"not_working", "login not working", IntentError},
{"crash", "application crash on startup", IntentError},
// Implementation intents
{"implement", "implement user authentication", IntentImplementation},
{"add_feature", "add new endpoint for users", IntentImplementation},
{"create", "create a handler for uploads", IntentImplementation},
{"function", "function to validate input", IntentImplementation},
// Architecture intents
{"architecture", "architecture of the system", IntentArchitecture},
{"design", "design pattern for observers", IntentArchitecture},
{"component", "component structure", IntentArchitecture},
{"flow", "data flow in the pipeline", IntentArchitecture},
// General intents
{"general", "user authentication", IntentGeneral},
{"empty", "", IntentGeneral},
{"simple", "database", IntentGeneral},
}
for _, tt := range tests {
s.Run(tt.name, func() {
result := s.expander.DetectIntent(tt.query)
s.Equal(tt.expected, result, "Query: %s", tt.query)
})
}
}
// TestExpand tests basic query expansion.
func (s *ExpanderSuite) TestExpand() {
ctx := context.Background()
cfg := DefaultConfig()
cfg.EnableVocabularyExpansion = false // Disable for unit test
tests := []struct {
name string
query string
minExpansions int
hasOriginal bool
expectedIntent QueryIntent
}{
{"question", "how do I implement auth", 1, true, IntentQuestion},
{"error", "fix the bug in login", 1, true, IntentError},
{"implementation", "implement user handler", 1, true, IntentImplementation},
{"architecture", "architecture design", 1, true, IntentArchitecture},
{"general", "database connection", 1, true, IntentGeneral},
{"empty", "", 0, false, IntentGeneral},
}
for _, tt := range tests {
s.Run(tt.name, func() {
expansions := s.expander.Expand(ctx, tt.query, cfg)
if tt.minExpansions == 0 {
s.Empty(expansions)
return
}
s.GreaterOrEqual(len(expansions), tt.minExpansions)
if tt.hasOriginal {
// First expansion should be the original
s.Equal(tt.query, expansions[0].Query)
s.Equal(1.0, expansions[0].Weight)
s.Equal("original", expansions[0].Source)
}
})
}
}
// TestExpandWithConfig tests expansion with custom config.
func (s *ExpanderSuite) TestExpandWithConfig() {
ctx := context.Background()
cfg := Config{
MaxExpansions: 2,
MinSimilarity: 0.7,
EnableVocabularyExpansion: false,
}
expansions := s.expander.Expand(ctx, "how to implement authentication", cfg)
s.LessOrEqual(len(expansions), cfg.MaxExpansions)
}
// TestExpandDeduplication tests that duplicates are removed.
func (s *ExpanderSuite) TestExpandDeduplication() {
ctx := context.Background()
cfg := DefaultConfig()
cfg.EnableVocabularyExpansion = false
// Query that might generate duplicate expansions
query := "how to fix authentication"
expansions := s.expander.Expand(ctx, query, cfg)
// Check for duplicates
seen := make(map[string]bool)
for _, exp := range expansions {
normalized := exp.Query
s.False(seen[normalized], "Duplicate expansion found: %s", exp.Query)
seen[normalized] = true
}
}
// TestExtractKeyTerms tests key term extraction.
func TestExtractKeyTerms(t *testing.T) {
tests := []struct {
name string
query string
expected []string
}{
{
name: "simple",
query: "user authentication handler",
expected: []string{"user", "authentication", "handler"},
},
{
name: "with_stop_words",
query: "how to implement the user login",
expected: []string{"implement", "user", "login"},
},
{
name: "with_punctuation",
query: "fix the bug, please!",
expected: []string{"fix", "bug", "please"},
},
{
name: "empty",
query: "",
expected: nil,
},
{
name: "only_stop_words",
query: "the a an is are",
expected: nil,
},
{
name: "short_words_filtered",
query: "a b c auth",
expected: []string{"auth"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := extractKeyTerms(tt.query)
if tt.expected == nil {
assert.Empty(t, result)
} else {
assert.Equal(t, tt.expected, result)
}
})
}
}
// TestMakeDeclarative tests question to declarative conversion.
func TestMakeDeclarative(t *testing.T) {
tests := []struct {
name string
query string
expected string
}{
{
name: "how_do_i",
query: "how do I implement auth?",
expected: "implement auth",
},
{
name: "how_to",
query: "how to fix the bug",
expected: "fix the bug",
},
{
name: "what_is",
query: "what is the purpose of this?",
expected: "the purpose of this",
},
{
name: "why_does",
query: "why does this fail?",
expected: "this fail",
},
{
name: "already_declarative",
query: "user authentication",
expected: "user authentication",
},
{
name: "question_mark_only",
query: "authentication?",
expected: "authentication",
},
{
name: "case_insensitive",
query: "How To Fix Auth?",
expected: "Fix Auth",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := makeDeclarative(tt.query)
assert.Equal(t, tt.expected, result)
})
}
}
// TestDeduplicateExpansions tests deduplication.
func TestDeduplicateExpansions(t *testing.T) {
expansions := []ExpandedQuery{
{Query: "auth handler", Weight: 1.0},
{Query: "AUTH HANDLER", Weight: 0.8}, // Duplicate (case insensitive)
{Query: "auth handler ", Weight: 0.7}, // Duplicate (whitespace)
{Query: "user auth", Weight: 0.6},
}
result := deduplicateExpansions(expansions)
assert.Len(t, result, 2) // "auth handler" and "user auth"
assert.Equal(t, "auth handler", result[0].Query)
assert.Equal(t, 1.0, result[0].Weight) // First one preserved
assert.Equal(t, "user auth", result[1].Query)
}
// TestCosineSimilarity tests cosine similarity calculation.
func TestCosineSimilarity(t *testing.T) {
tests := []struct {
name string
a []float32
b []float32
expected float64
delta float64
}{
{
name: "identical_vectors",
a: []float32{1, 0, 0},
b: []float32{1, 0, 0},
expected: 1.0,
delta: 0.001,
},
{
name: "orthogonal_vectors",
a: []float32{1, 0, 0},
b: []float32{0, 1, 0},
expected: 0.0,
delta: 0.001,
},
{
name: "opposite_vectors",
a: []float32{1, 0, 0},
b: []float32{-1, 0, 0},
expected: -1.0,
delta: 0.001,
},
{
name: "similar_vectors",
a: []float32{1, 1, 0},
b: []float32{1, 0, 0},
expected: 0.707,
delta: 0.01,
},
{
name: "empty_vectors",
a: []float32{},
b: []float32{},
expected: 0.0,
delta: 0.001,
},
{
name: "different_lengths",
a: []float32{1, 0},
b: []float32{1, 0, 0},
expected: 0.0,
delta: 0.001,
},
{
name: "zero_vector",
a: []float32{0, 0, 0},
b: []float32{1, 1, 1},
expected: 0.0,
delta: 0.001,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := cosineSimilarity(tt.a, tt.b)
assert.InDelta(t, tt.expected, result, tt.delta)
})
}
}
// TestDefaultConfig tests default configuration.
func TestDefaultConfig(t *testing.T) {
cfg := DefaultConfig()
assert.Equal(t, 4, cfg.MaxExpansions)
assert.Equal(t, 0.5, cfg.MinSimilarity)
assert.True(t, cfg.EnableVocabularyExpansion)
}
// TestExpandedQueryStruct tests ExpandedQuery struct.
func TestExpandedQueryStruct(t *testing.T) {
eq := ExpandedQuery{
Query: "test query",
Weight: 0.85,
Source: "vocabulary:auth",
Intent: IntentQuestion,
}
assert.Equal(t, "test query", eq.Query)
assert.Equal(t, 0.85, eq.Weight)
assert.Equal(t, "vocabulary:auth", eq.Source)
assert.Equal(t, IntentQuestion, eq.Intent)
}
// TestVocabEntry tests VocabEntry struct.
func TestVocabEntry(t *testing.T) {
ve := VocabEntry{
Term: "authentication",
Weight: 0.9,
Source: "concept",
}
assert.Equal(t, "authentication", ve.Term)
assert.Equal(t, 0.9, ve.Weight)
assert.Equal(t, "concept", ve.Source)
}
// TestIntentConstants tests intent constant values.
func TestIntentConstants(t *testing.T) {
assert.Equal(t, QueryIntent("question"), IntentQuestion)
assert.Equal(t, QueryIntent("error"), IntentError)
assert.Equal(t, QueryIntent("implementation"), IntentImplementation)
assert.Equal(t, QueryIntent("architecture"), IntentArchitecture)
assert.Equal(t, QueryIntent("general"), IntentGeneral)
}
// TestTruncate tests the truncate helper.
func TestTruncate(t *testing.T) {
tests := []struct {
name string
input string
maxLen int
expected string
}{
{"short", "hello", 10, "hello"},
{"exact", "hello", 5, "hello"},
{"long", "hello world", 5, "hello..."},
{"empty", "", 10, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := truncate(tt.input, tt.maxLen)
assert.Equal(t, tt.expected, result)
})
}
}
// TestSqrt tests the sqrt helper.
func TestSqrt(t *testing.T) {
tests := []struct {
input float64
expected float64
delta float64
}{
{4.0, 2.0, 0.001},
{9.0, 3.0, 0.001},
{16.0, 4.0, 0.001},
{2.0, 1.414, 0.01},
{0.0, 0.0, 0.001},
{-1.0, 0.0, 0.001},
}
for _, tt := range tests {
t.Run("", func(t *testing.T) {
result := sqrt(tt.input)
assert.InDelta(t, tt.expected, result, tt.delta)
})
}
}
// TestExpandByIntentError tests error intent expansion.
func (s *ExpanderSuite) TestExpandByIntentError() {
expansions := s.expander.expandByIntent("fix authentication bug", IntentError)
s.NotEmpty(expansions)
// Should have solution-oriented expansion
hasSolution := false
for _, exp := range expansions {
if exp.Source == "intent:solution" {
hasSolution = true
break
}
}
s.True(hasSolution)
}
// TestExpandByIntentQuestion tests question intent expansion.
func (s *ExpanderSuite) TestExpandByIntentQuestion() {
expansions := s.expander.expandByIntent("how do I implement auth", IntentQuestion)
s.NotEmpty(expansions)
// Should have declarative expansion
hasDeclarative := false
for _, exp := range expansions {
if exp.Source == "intent:declarative" {
hasDeclarative = true
break
}
}
s.True(hasDeclarative)
}
// TestExpandByIntentImplementation tests implementation intent expansion.
func (s *ExpanderSuite) TestExpandByIntentImplementation() {
expansions := s.expander.expandByIntent("implement user handler", IntentImplementation)
s.NotEmpty(expansions)
// Should have how expansion
hasHow := false
for _, exp := range expansions {
if exp.Source == "intent:how" {
hasHow = true
break
}
}
s.True(hasHow)
}
// TestExpandByIntentArchitecture tests architecture intent expansion.
func (s *ExpanderSuite) TestExpandByIntentArchitecture() {
expansions := s.expander.expandByIntent("system architecture design", IntentArchitecture)
s.NotEmpty(expansions)
// Should have design expansion
hasDesign := false
for _, exp := range expansions {
if exp.Source == "intent:design" {
hasDesign = true
break
}
}
s.True(hasDesign)
}
// TestExpandByIntentGeneral tests general intent returns no expansions.
func (s *ExpanderSuite) TestExpandByIntentGeneral() {
expansions := s.expander.expandByIntent("database", IntentGeneral)
s.Empty(expansions) // General intent doesn't add intent-based expansions
}
// TestEmptyVocabulary tests expansion with empty vocabulary.
func (s *ExpanderSuite) TestEmptyVocabulary() {
ctx := context.Background()
expansions := s.expander.expandByVocabulary(ctx, "test query", 0.5)
s.Empty(expansions)
}
// TestIntentPatternsExist tests that all intents have patterns.
func (s *ExpanderSuite) TestIntentPatternsExist() {
s.NotEmpty(s.expander.intentPatterns[IntentQuestion])
s.NotEmpty(s.expander.intentPatterns[IntentError])
s.NotEmpty(s.expander.intentPatterns[IntentImplementation])
s.NotEmpty(s.expander.intentPatterns[IntentArchitecture])
}
+29 -15
View File
@@ -35,20 +35,21 @@ func NewManager(
// SearchParams contains parameters for unified search.
type SearchParams struct {
Query string
Type string // "observations", "sessions", "prompts", or empty for all
Project string
ObsType string // Observation type filter
Concepts string
Files string
DateStart int64
DateEnd int64
OrderBy string // "relevance", "date_desc", "date_asc"
Limit int
Offset int
Format string // "index" or "full"
Scope string // "project", "global", or empty for project+global
IncludeGlobal bool // If true, include global observations along with project-scoped
Query string
Type string // "observations", "sessions", "prompts", or empty for all
Project string
ObsType string // Observation type filter
Concepts string
Files string
DateStart int64
DateEnd int64
OrderBy string // "relevance", "date_desc", "date_asc"
Limit int
Offset int
Format string // "index" or "full"
Scope string // "project", "global", or empty for project+global
IncludeGlobal bool // If true, include global observations along with project-scoped
ExcludeSuperseded bool // If true, exclude observations that have been superseded
}
// SearchResult represents a unified search result.
@@ -126,6 +127,10 @@ func (m *Manager) vectorSearch(ctx context.Context, params SearchParams) (*Unifi
obs, err := m.observationStore.GetObservationsByIDs(ctx, obsIDs, params.OrderBy, 0)
if err == nil {
for _, o := range obs {
// Skip superseded observations when requested
if params.ExcludeSuperseded && o.IsSuperseded {
continue
}
results = append(results, m.observationToResult(o, params.Format))
}
}
@@ -167,7 +172,16 @@ func (m *Manager) filterSearch(ctx context.Context, params SearchParams) (*Unifi
// Search observations
if params.Type == "" || params.Type == "observations" {
obs, err := m.observationStore.GetRecentObservations(ctx, params.Project, params.Limit)
var obs []*models.Observation
var err error
// Use active observations (excluding superseded) when requested
if params.ExcludeSuperseded {
obs, err = m.observationStore.GetActiveObservations(ctx, params.Project, params.Limit)
} else {
obs, err = m.observationStore.GetRecentObservations(ctx, params.Project, params.Limit)
}
if err == nil {
for _, o := range obs {
results = append(results, m.observationToResult(o, params.Format))