mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-05 23:03:55 +00:00
7a061c85eb
* refactor(hooks): simplify hook execution with shared context - [x] Extract BaseInput struct to eliminate duplicate fields across hooks - [x] Create RunHook handler pattern for session-start and user-prompt - [x] Create RunStatuslineHook for fast statusline rendering without worker startup - [x] Add HookContext struct to pass port, project, CWD, SessionID to handlers - [x] Add db/interface.go with ObservationReader/Writer interfaces - [x] Add comprehensive conflict management tests in sqlite/conflict_test.go - [x] Add vector client tests for Count, ModelVersion, NeedsRebuild, GetStaleVectors - [x] Add FilterByThreshold helper tests for query result filtering - [x] Make handlers_test more robust for network-dependent update checks - [x] Update package versions in UI * Move to GORM + general cleanup * feat(mcp): add observation relations discovery and scoring integration - [x] Add find_related_observations MCP tool for discovering related observations by confidence - [x] Integrate scoring calculator and recalculator into MCP server initialization - [x] Add pattern, relation, and session stores to MCP server dependencies - [x] Register MCP server in Claude Code settings during plugin installation - [x] Update install scripts (bash, PowerShell) to configure MCP server settings - [x] Switch plugin manifest files to template-based versioning (plugin.json.tpl, marketplace.json.tpl) - [x] Update all MCP server tests to pass new dependency parameters
421 lines
11 KiB
Go
421 lines
11 KiB
Go
// Package gorm provides GORM-based database operations for claude-mnemonic.
|
|
package gorm
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/lukaszraczylo/claude-mnemonic/pkg/models"
|
|
)
|
|
|
|
// PatternCleanupFunc is a callback for when patterns are deleted.
|
|
type PatternCleanupFunc func(ctx context.Context, deletedIDs []int64)
|
|
|
|
// PatternStore provides pattern-related database operations using GORM.
|
|
type PatternStore struct {
|
|
db *gorm.DB
|
|
cleanupFunc PatternCleanupFunc
|
|
}
|
|
|
|
// NewPatternStore creates a new pattern store.
|
|
func NewPatternStore(store *Store) *PatternStore {
|
|
return &PatternStore{
|
|
db: store.DB,
|
|
}
|
|
}
|
|
|
|
// SetCleanupFunc sets the callback for when patterns are deleted.
|
|
func (s *PatternStore) SetCleanupFunc(fn PatternCleanupFunc) {
|
|
s.cleanupFunc = fn
|
|
}
|
|
|
|
// StorePattern stores a new pattern.
|
|
func (s *PatternStore) StorePattern(ctx context.Context, pattern *models.Pattern) (int64, error) {
|
|
dbPattern := &Pattern{
|
|
Name: pattern.Name,
|
|
Type: pattern.Type,
|
|
Signature: pattern.Signature,
|
|
Frequency: pattern.Frequency,
|
|
Projects: pattern.Projects,
|
|
ObservationIDs: pattern.ObservationIDs,
|
|
Status: pattern.Status,
|
|
Confidence: pattern.Confidence,
|
|
LastSeenAt: pattern.LastSeenAt,
|
|
LastSeenAtEpoch: pattern.LastSeenEpoch,
|
|
CreatedAt: pattern.CreatedAt,
|
|
CreatedAtEpoch: pattern.CreatedAtEpoch,
|
|
}
|
|
|
|
if pattern.Description.Valid {
|
|
dbPattern.Description = sql.NullString{String: pattern.Description.String, Valid: true}
|
|
}
|
|
|
|
if pattern.Recommendation.Valid {
|
|
dbPattern.Recommendation = sql.NullString{String: pattern.Recommendation.String, Valid: true}
|
|
}
|
|
|
|
if pattern.MergedIntoID.Valid {
|
|
dbPattern.MergedIntoID = sql.NullInt64{Int64: pattern.MergedIntoID.Int64, Valid: true}
|
|
}
|
|
|
|
result := s.db.WithContext(ctx).Create(dbPattern)
|
|
if result.Error != nil {
|
|
return 0, result.Error
|
|
}
|
|
|
|
return dbPattern.ID, nil
|
|
}
|
|
|
|
// UpdatePattern updates an existing pattern.
|
|
func (s *PatternStore) UpdatePattern(ctx context.Context, pattern *models.Pattern) error {
|
|
updates := map[string]interface{}{
|
|
"name": pattern.Name,
|
|
"type": pattern.Type,
|
|
"signature": pattern.Signature,
|
|
"frequency": pattern.Frequency,
|
|
"projects": pattern.Projects,
|
|
"observation_ids": pattern.ObservationIDs,
|
|
"status": pattern.Status,
|
|
"confidence": pattern.Confidence,
|
|
"last_seen_at": pattern.LastSeenAt,
|
|
"last_seen_at_epoch": pattern.LastSeenEpoch,
|
|
}
|
|
|
|
if pattern.Description.Valid {
|
|
updates["description"] = pattern.Description.String
|
|
} else {
|
|
updates["description"] = nil
|
|
}
|
|
|
|
if pattern.Recommendation.Valid {
|
|
updates["recommendation"] = pattern.Recommendation.String
|
|
} else {
|
|
updates["recommendation"] = nil
|
|
}
|
|
|
|
if pattern.MergedIntoID.Valid {
|
|
updates["merged_into_id"] = pattern.MergedIntoID.Int64
|
|
} else {
|
|
updates["merged_into_id"] = nil
|
|
}
|
|
|
|
result := s.db.WithContext(ctx).
|
|
Model(&Pattern{}).
|
|
Where("id = ?", pattern.ID).
|
|
Updates(updates)
|
|
|
|
return result.Error
|
|
}
|
|
|
|
// GetPatternByID retrieves a pattern by ID.
|
|
func (s *PatternStore) GetPatternByID(ctx context.Context, id int64) (*models.Pattern, error) {
|
|
var dbPattern Pattern
|
|
|
|
err := s.db.WithContext(ctx).First(&dbPattern, id).Error
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return toModelPattern(&dbPattern), nil
|
|
}
|
|
|
|
// GetPatternByName retrieves a pattern by name.
|
|
func (s *PatternStore) GetPatternByName(ctx context.Context, name string) (*models.Pattern, error) {
|
|
var dbPattern Pattern
|
|
|
|
err := s.db.WithContext(ctx).
|
|
Where("name = ? AND status = ?", name, models.PatternStatusActive).
|
|
First(&dbPattern).Error
|
|
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return toModelPattern(&dbPattern), nil
|
|
}
|
|
|
|
// GetActivePatterns retrieves all active patterns.
|
|
func (s *PatternStore) GetActivePatterns(ctx context.Context, limit int) ([]*models.Pattern, error) {
|
|
var patterns []Pattern
|
|
|
|
err := s.db.WithContext(ctx).
|
|
Where("status = ?", models.PatternStatusActive).
|
|
Order("frequency DESC, confidence DESC").
|
|
Limit(limit).
|
|
Find(&patterns).Error
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return toModelPatterns(patterns), nil
|
|
}
|
|
|
|
// GetPatternsByType retrieves patterns of a specific type.
|
|
func (s *PatternStore) GetPatternsByType(ctx context.Context, patternType models.PatternType, limit int) ([]*models.Pattern, error) {
|
|
var patterns []Pattern
|
|
|
|
err := s.db.WithContext(ctx).
|
|
Where("type = ? AND status = ?", patternType, models.PatternStatusActive).
|
|
Order("frequency DESC, confidence DESC").
|
|
Limit(limit).
|
|
Find(&patterns).Error
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return toModelPatterns(patterns), nil
|
|
}
|
|
|
|
// GetPatternsByProject retrieves patterns that have been observed in a specific project.
|
|
// Uses raw SQL since JSON_EACH is complex in GORM.
|
|
func (s *PatternStore) GetPatternsByProject(ctx context.Context, project string, limit int) ([]*models.Pattern, error) {
|
|
var patterns []Pattern
|
|
|
|
// Use raw SQL for JSON_EACH query
|
|
query := `
|
|
SELECT * FROM patterns
|
|
WHERE status = 'active'
|
|
AND EXISTS (
|
|
SELECT 1 FROM json_each(projects)
|
|
WHERE json_each.value = ?
|
|
)
|
|
ORDER BY frequency DESC, confidence DESC
|
|
LIMIT ?
|
|
`
|
|
|
|
err := s.db.WithContext(ctx).
|
|
Raw(query, project, limit).
|
|
Scan(&patterns).Error
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return toModelPatterns(patterns), nil
|
|
}
|
|
|
|
// FindMatchingPatterns searches for patterns that match a given signature.
|
|
// Pattern matching is done in Go code for simplicity.
|
|
func (s *PatternStore) FindMatchingPatterns(ctx context.Context, signature []string, minScore float64) ([]*models.Pattern, error) {
|
|
// Get all active patterns
|
|
patterns, err := s.GetActivePatterns(ctx, 100)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Filter by signature match in Go
|
|
var matches []*models.Pattern
|
|
for _, pattern := range patterns {
|
|
score := models.CalculateMatchScore(signature, pattern.Signature)
|
|
if score >= minScore {
|
|
matches = append(matches, pattern)
|
|
}
|
|
}
|
|
|
|
return matches, nil
|
|
}
|
|
|
|
// MarkPatternDeprecated marks a pattern as deprecated.
|
|
func (s *PatternStore) MarkPatternDeprecated(ctx context.Context, id int64) error {
|
|
result := s.db.WithContext(ctx).
|
|
Model(&Pattern{}).
|
|
Where("id = ?", id).
|
|
Update("status", models.PatternStatusDeprecated)
|
|
|
|
return result.Error
|
|
}
|
|
|
|
// MergePatterns merges a source pattern into a target pattern.
|
|
func (s *PatternStore) MergePatterns(ctx context.Context, sourceID, targetID int64) error {
|
|
// Get both patterns
|
|
source, err := s.GetPatternByID(ctx, sourceID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
target, err := s.GetPatternByID(ctx, targetID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Merge source into target
|
|
target.Frequency += source.Frequency
|
|
|
|
// Merge projects (deduplicate)
|
|
for _, proj := range source.Projects {
|
|
found := false
|
|
for _, existing := range target.Projects {
|
|
if existing == proj {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
target.Projects = append(target.Projects, proj)
|
|
}
|
|
}
|
|
|
|
// Merge observation IDs (deduplicate)
|
|
for _, obsID := range source.ObservationIDs {
|
|
found := false
|
|
for _, existing := range target.ObservationIDs {
|
|
if existing == obsID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
target.ObservationIDs = append(target.ObservationIDs, obsID)
|
|
}
|
|
}
|
|
|
|
// Update target
|
|
if err := s.UpdatePattern(ctx, target); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Mark source as merged
|
|
source.Status = models.PatternStatusMerged
|
|
source.MergedIntoID = sql.NullInt64{Int64: targetID, Valid: true}
|
|
return s.UpdatePattern(ctx, source)
|
|
}
|
|
|
|
// DeletePattern deletes a pattern by ID.
|
|
func (s *PatternStore) DeletePattern(ctx context.Context, id int64) error {
|
|
result := s.db.WithContext(ctx).Delete(&Pattern{}, id)
|
|
|
|
if result.Error == nil && s.cleanupFunc != nil {
|
|
s.cleanupFunc(ctx, []int64{id})
|
|
}
|
|
|
|
return result.Error
|
|
}
|
|
|
|
// SearchPatternsFTS performs full-text search on patterns.
|
|
// Uses raw SQL for FTS5 query.
|
|
func (s *PatternStore) SearchPatternsFTS(ctx context.Context, searchQuery string, limit int) ([]*models.Pattern, error) {
|
|
var patterns []Pattern
|
|
|
|
// Use raw SQL for FTS5 MATCH query
|
|
query := `
|
|
SELECT p.*
|
|
FROM patterns p
|
|
JOIN patterns_fts fts ON p.id = fts.rowid
|
|
WHERE patterns_fts MATCH ?
|
|
AND p.status = 'active'
|
|
ORDER BY rank
|
|
LIMIT ?
|
|
`
|
|
|
|
err := s.db.WithContext(ctx).
|
|
Raw(query, searchQuery, limit).
|
|
Scan(&patterns).Error
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return toModelPatterns(patterns), nil
|
|
}
|
|
|
|
// PatternStats contains aggregate statistics about patterns.
|
|
type PatternStats struct {
|
|
Total int `json:"total"`
|
|
Active int `json:"active"`
|
|
Deprecated int `json:"deprecated"`
|
|
Merged int `json:"merged"`
|
|
TotalOccurrences int `json:"total_occurrences"`
|
|
AvgConfidence float64 `json:"avg_confidence"`
|
|
Bugs int `json:"bugs"`
|
|
Refactors int `json:"refactors"`
|
|
Architectures int `json:"architectures"`
|
|
AntiPatterns int `json:"anti_patterns"`
|
|
BestPractices int `json:"best_practices"`
|
|
}
|
|
|
|
// GetPatternStats returns statistics about patterns.
|
|
// Uses raw SQL for complex aggregate query.
|
|
func (s *PatternStore) GetPatternStats(ctx context.Context) (*PatternStats, error) {
|
|
var stats PatternStats
|
|
|
|
query := `
|
|
SELECT
|
|
COUNT(*) as total,
|
|
COUNT(CASE WHEN status = 'active' THEN 1 END) as active,
|
|
COUNT(CASE WHEN status = 'deprecated' THEN 1 END) as deprecated,
|
|
COUNT(CASE WHEN status = 'merged' THEN 1 END) as merged,
|
|
COALESCE(SUM(frequency), 0) as total_occurrences,
|
|
COALESCE(AVG(confidence), 0) as avg_confidence,
|
|
COUNT(CASE WHEN type = 'bug' THEN 1 END) as bugs,
|
|
COUNT(CASE WHEN type = 'refactor' THEN 1 END) as refactors,
|
|
COUNT(CASE WHEN type = 'architecture' THEN 1 END) as architectures,
|
|
COUNT(CASE WHEN type = 'anti-pattern' THEN 1 END) as anti_patterns,
|
|
COUNT(CASE WHEN type = 'best-practice' THEN 1 END) as best_practices
|
|
FROM patterns
|
|
`
|
|
|
|
err := s.db.WithContext(ctx).Raw(query).Scan(&stats).Error
|
|
return &stats, err
|
|
}
|
|
|
|
// IncrementPatternFrequency atomically increments a pattern's frequency and updates last_seen.
|
|
func (s *PatternStore) IncrementPatternFrequency(ctx context.Context, id int64, project string, observationID int64) error {
|
|
now := time.Now()
|
|
|
|
// Get current pattern
|
|
pattern, err := s.GetPatternByID(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add occurrence
|
|
pattern.AddOccurrence(project, observationID)
|
|
pattern.LastSeenAt = now.Format(time.RFC3339)
|
|
pattern.LastSeenEpoch = now.UnixMilli()
|
|
|
|
return s.UpdatePattern(ctx, pattern)
|
|
}
|
|
|
|
// toModelPattern converts a GORM Pattern to a pkg/models Pattern.
|
|
func toModelPattern(p *Pattern) *models.Pattern {
|
|
pattern := &models.Pattern{
|
|
ID: p.ID,
|
|
Name: p.Name,
|
|
Type: p.Type,
|
|
Description: p.Description,
|
|
Signature: p.Signature,
|
|
Recommendation: p.Recommendation,
|
|
Frequency: p.Frequency,
|
|
Projects: p.Projects,
|
|
ObservationIDs: p.ObservationIDs,
|
|
Status: p.Status,
|
|
MergedIntoID: p.MergedIntoID,
|
|
Confidence: p.Confidence,
|
|
LastSeenAt: p.LastSeenAt,
|
|
LastSeenEpoch: p.LastSeenAtEpoch,
|
|
CreatedAt: p.CreatedAt,
|
|
CreatedAtEpoch: p.CreatedAtEpoch,
|
|
}
|
|
|
|
return pattern
|
|
}
|
|
|
|
// toModelPatterns converts a slice of GORM Patterns to pkg/models Patterns.
|
|
func toModelPatterns(patterns []Pattern) []*models.Pattern {
|
|
result := make([]*models.Pattern, len(patterns))
|
|
for i, p := range patterns {
|
|
result[i] = toModelPattern(&p)
|
|
}
|
|
return result
|
|
}
|