Files
claude-mnemonic/internal/db/gorm/migrations.go
T
lukaszraczylo 7a061c85eb general improvements (#17)
* 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
2026-01-07 00:26:20 +00:00

333 lines
11 KiB
Go

// Package gorm provides GORM-based database operations for claude-mnemonic.
package gorm
import (
"database/sql"
"fmt"
"time"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// runMigrations runs all database migrations using gormigrate.
func runMigrations(db *gorm.DB, sqlDB *sql.DB) error {
m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
// Migration 001: Core tables (SDKSession, Observation, SessionSummary)
{
ID: "001_core_tables",
Migrate: func(tx *gorm.DB) error {
// AutoMigrate creates tables with all indexes from struct tags
if err := tx.AutoMigrate(&SDKSession{}); err != nil {
return err
}
if err := tx.AutoMigrate(&Observation{}); err != nil {
return err
}
return tx.AutoMigrate(&SessionSummary{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("sdk_sessions", "observations", "session_summaries")
},
},
// Migration 002: User prompts table
{
ID: "002_user_prompts",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&UserPrompt{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("user_prompts")
},
},
// Migration 003: FTS5 virtual table for user prompts
{
ID: "003_user_prompts_fts",
Migrate: func(tx *gorm.DB) error {
sqls := []string{
`CREATE VIRTUAL TABLE IF NOT EXISTS user_prompts_fts USING fts5(
prompt_text,
content='user_prompts',
content_rowid='id'
)`,
`CREATE TRIGGER IF NOT EXISTS user_prompts_ai AFTER INSERT ON user_prompts BEGIN
INSERT INTO user_prompts_fts(rowid, prompt_text)
VALUES (new.id, new.prompt_text);
END`,
`CREATE TRIGGER IF NOT EXISTS user_prompts_ad AFTER DELETE ON user_prompts BEGIN
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
VALUES('delete', old.id, old.prompt_text);
END`,
`CREATE TRIGGER IF NOT EXISTS user_prompts_au AFTER UPDATE ON user_prompts BEGIN
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
VALUES('delete', old.id, old.prompt_text);
INSERT INTO user_prompts_fts(rowid, prompt_text)
VALUES (new.id, new.prompt_text);
END`,
}
for _, s := range sqls {
if err := tx.Exec(s).Error; err != nil {
return err
}
}
return nil
},
Rollback: func(tx *gorm.DB) error {
sqls := []string{
"DROP TRIGGER IF EXISTS user_prompts_au",
"DROP TRIGGER IF EXISTS user_prompts_ad",
"DROP TRIGGER IF EXISTS user_prompts_ai",
"DROP TABLE IF EXISTS user_prompts_fts",
}
for _, s := range sqls {
if err := tx.Exec(s).Error; err != nil {
return err
}
}
return nil
},
},
// Migration 004: FTS5 virtual table for observations
{
ID: "004_observations_fts",
Migrate: func(tx *gorm.DB) error {
sqls := []string{
`CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
title, subtitle, narrative,
content='observations',
content_rowid='id'
)`,
`CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
INSERT INTO observations_fts(rowid, title, subtitle, narrative)
VALUES (new.id, new.title, new.subtitle, new.narrative);
END`,
`CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative)
VALUES('delete', old.id, old.title, old.subtitle, old.narrative);
END`,
`CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative)
VALUES('delete', old.id, old.title, old.subtitle, old.narrative);
INSERT INTO observations_fts(rowid, title, subtitle, narrative)
VALUES (new.id, new.title, new.subtitle, new.narrative);
END`,
}
for _, s := range sqls {
if err := tx.Exec(s).Error; err != nil {
return err
}
}
return nil
},
Rollback: func(tx *gorm.DB) error {
sqls := []string{
"DROP TRIGGER IF EXISTS observations_au",
"DROP TRIGGER IF EXISTS observations_ad",
"DROP TRIGGER IF EXISTS observations_ai",
"DROP TABLE IF EXISTS observations_fts",
}
for _, s := range sqls {
if err := tx.Exec(s).Error; err != nil {
return err
}
}
return nil
},
},
// Migration 005: FTS5 virtual table for session summaries
{
ID: "005_session_summaries_fts",
Migrate: func(tx *gorm.DB) error {
sqls := []string{
`CREATE VIRTUAL TABLE IF NOT EXISTS session_summaries_fts USING fts5(
request, investigated, learned, completed, next_steps, notes,
content='session_summaries',
content_rowid='id'
)`,
`CREATE TRIGGER IF NOT EXISTS session_summaries_ai AFTER INSERT ON session_summaries BEGIN
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
END`,
`CREATE TRIGGER IF NOT EXISTS session_summaries_ad AFTER DELETE ON session_summaries BEGIN
INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
END`,
`CREATE TRIGGER IF NOT EXISTS session_summaries_au AFTER UPDATE ON session_summaries BEGIN
INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
END`,
}
for _, s := range sqls {
if err := tx.Exec(s).Error; err != nil {
return err
}
}
return nil
},
Rollback: func(tx *gorm.DB) error {
sqls := []string{
"DROP TRIGGER IF EXISTS session_summaries_au",
"DROP TRIGGER IF EXISTS session_summaries_ad",
"DROP TRIGGER IF EXISTS session_summaries_ai",
"DROP TABLE IF EXISTS session_summaries_fts",
}
for _, s := range sqls {
if err := tx.Exec(s).Error; err != nil {
return err
}
}
return nil
},
},
// Migration 006: sqlite-vec vectors table
{
ID: "006_sqlite_vec_vectors",
Migrate: func(tx *gorm.DB) error {
// Note: Uses bge-small-en-v1.5 embeddings (384 dimensions) with model_version
sql := `CREATE VIRTUAL TABLE IF NOT EXISTS vectors USING vec0(
doc_id TEXT PRIMARY KEY,
embedding float[384],
sqlite_id INTEGER,
doc_type TEXT,
field_type TEXT,
project TEXT,
scope TEXT,
model_version TEXT
)`
return tx.Exec(sql).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Exec("DROP TABLE IF EXISTS vectors").Error
},
},
// Migration 007: Concept weights table with seed data
{
ID: "007_concept_weights",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&ConceptWeight{}); err != nil {
return err
}
// Seed default concept weights
now := time.Now().Format(time.RFC3339)
weights := []ConceptWeight{
{Concept: "security", Weight: 0.30, UpdatedAt: now},
{Concept: "gotcha", Weight: 0.25, UpdatedAt: now},
{Concept: "best-practice", Weight: 0.20, UpdatedAt: now},
{Concept: "anti-pattern", Weight: 0.20, UpdatedAt: now},
{Concept: "architecture", Weight: 0.15, UpdatedAt: now},
{Concept: "performance", Weight: 0.15, UpdatedAt: now},
{Concept: "error-handling", Weight: 0.15, UpdatedAt: now},
{Concept: "pattern", Weight: 0.10, UpdatedAt: now},
{Concept: "testing", Weight: 0.10, UpdatedAt: now},
{Concept: "debugging", Weight: 0.10, UpdatedAt: now},
{Concept: "workflow", Weight: 0.05, UpdatedAt: now},
{Concept: "tooling", Weight: 0.05, UpdatedAt: now},
}
// INSERT OR IGNORE equivalent in GORM
return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&weights).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("concept_weights")
},
},
// Migration 008: Observation conflicts table
{
ID: "008_observation_conflicts",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&ObservationConflict{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("observation_conflicts")
},
},
// Migration 009: Patterns table
{
ID: "009_patterns",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&Pattern{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("patterns")
},
},
// Migration 010: FTS5 virtual table for patterns
{
ID: "010_patterns_fts",
Migrate: func(tx *gorm.DB) error {
sqls := []string{
`CREATE VIRTUAL TABLE IF NOT EXISTS patterns_fts USING fts5(
name, description, recommendation,
content='patterns',
content_rowid='id'
)`,
`CREATE TRIGGER IF NOT EXISTS patterns_ai AFTER INSERT ON patterns BEGIN
INSERT INTO patterns_fts(rowid, name, description, recommendation)
VALUES (new.id, new.name, new.description, new.recommendation);
END`,
`CREATE TRIGGER IF NOT EXISTS patterns_ad AFTER DELETE ON patterns BEGIN
INSERT INTO patterns_fts(patterns_fts, rowid, name, description, recommendation)
VALUES('delete', old.id, old.name, old.description, old.recommendation);
END`,
`CREATE TRIGGER IF NOT EXISTS patterns_au AFTER UPDATE ON patterns BEGIN
INSERT INTO patterns_fts(patterns_fts, rowid, name, description, recommendation)
VALUES('delete', old.id, old.name, old.description, old.recommendation);
INSERT INTO patterns_fts(rowid, name, description, recommendation)
VALUES (new.id, new.name, new.description, new.recommendation);
END`,
}
for _, s := range sqls {
if err := tx.Exec(s).Error; err != nil {
return err
}
}
return nil
},
Rollback: func(tx *gorm.DB) error {
sqls := []string{
"DROP TRIGGER IF EXISTS patterns_au",
"DROP TRIGGER IF EXISTS patterns_ad",
"DROP TRIGGER IF EXISTS patterns_ai",
"DROP TABLE IF EXISTS patterns_fts",
}
for _, s := range sqls {
if err := tx.Exec(s).Error; err != nil {
return err
}
}
return nil
},
},
// Migration 011: Observation relations table
{
ID: "011_observation_relations",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&ObservationRelation{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("observation_relations")
},
},
})
if err := m.Migrate(); err != nil {
return fmt.Errorf("run gormigrate migrations: %w", err)
}
return nil
}