mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-08 23:39:40 +00:00
4f4b4ac70f
- [x] Add language-specific chunkers with AST parsing (Go, Python, TypeScript) - [x] Implement chunking manager to dispatch files to appropriate chunkers - [x] Integrate code chunks into vector sync for semantic search - [x] Add tree-sitter dependency for Python/TypeScript parsing - [x] Reorder struct fields for consistency across codebase - [x] Rename error variables to follow Go conventions (err → unmarshalErr, etc.) - [x] Add code chunk metadata to vector documents (language, symbol name, line ranges) - [x] Update worker service to initialize chunking pipeline with all three languages
187 lines
4.7 KiB
Go
187 lines
4.7 KiB
Go
// Package scoring provides importance score calculation for observations.
|
|
package scoring
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/lukaszraczylo/claude-mnemonic/internal/db/sqlite"
|
|
"github.com/lukaszraczylo/claude-mnemonic/pkg/models"
|
|
)
|
|
|
|
// ObservationStore defines the interface for observation storage operations needed by the recalculator.
|
|
type ObservationStore interface {
|
|
GetObservationsNeedingScoreUpdate(ctx context.Context, threshold time.Duration, limit int) ([]*models.Observation, error)
|
|
UpdateImportanceScores(ctx context.Context, scores map[int64]float64) error
|
|
GetConceptWeights(ctx context.Context) (map[string]float64, error)
|
|
}
|
|
|
|
// Recalculator periodically recalculates importance scores for observations.
|
|
type Recalculator struct {
|
|
log zerolog.Logger
|
|
store ObservationStore
|
|
calculator *Calculator
|
|
stopCh chan struct{}
|
|
doneCh chan struct{}
|
|
interval time.Duration
|
|
batchSize int
|
|
mu sync.Mutex
|
|
running bool
|
|
}
|
|
|
|
// NewRecalculator creates a new background recalculator.
|
|
func NewRecalculator(store ObservationStore, calc *Calculator, log zerolog.Logger) *Recalculator {
|
|
return &Recalculator{
|
|
store: store,
|
|
calculator: calc,
|
|
log: log.With().Str("component", "recalculator").Logger(),
|
|
interval: 1 * time.Hour, // Run every hour
|
|
batchSize: 500, // Process 500 observations at a time
|
|
stopCh: make(chan struct{}),
|
|
doneCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Start begins the background recalculation loop.
|
|
// This should be called in a goroutine.
|
|
func (r *Recalculator) Start(ctx context.Context) {
|
|
r.mu.Lock()
|
|
if r.running {
|
|
r.mu.Unlock()
|
|
return
|
|
}
|
|
r.running = true
|
|
r.mu.Unlock()
|
|
|
|
defer func() {
|
|
r.mu.Lock()
|
|
r.running = false
|
|
r.mu.Unlock()
|
|
close(r.doneCh)
|
|
}()
|
|
|
|
// Initial run
|
|
r.recalculate(ctx)
|
|
|
|
r.mu.Lock()
|
|
interval := r.interval
|
|
r.mu.Unlock()
|
|
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
r.log.Info().Msg("recalculator shutting down due to context cancellation")
|
|
return
|
|
case <-r.stopCh:
|
|
r.log.Info().Msg("recalculator stopping")
|
|
return
|
|
case <-ticker.C:
|
|
r.recalculate(ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop stops the background recalculation loop.
|
|
func (r *Recalculator) Stop() {
|
|
r.mu.Lock()
|
|
if !r.running {
|
|
r.mu.Unlock()
|
|
return
|
|
}
|
|
r.mu.Unlock()
|
|
|
|
close(r.stopCh)
|
|
<-r.doneCh
|
|
}
|
|
|
|
// recalculate performs a single recalculation batch.
|
|
func (r *Recalculator) recalculate(ctx context.Context) {
|
|
now := time.Now()
|
|
threshold := r.calculator.RecalculateThreshold()
|
|
|
|
r.mu.Lock()
|
|
batchSize := r.batchSize
|
|
r.mu.Unlock()
|
|
|
|
observations, err := r.store.GetObservationsNeedingScoreUpdate(ctx, threshold, batchSize)
|
|
if err != nil {
|
|
r.log.Error().Err(err).Msg("failed to get observations for score update")
|
|
return
|
|
}
|
|
|
|
if len(observations) == 0 {
|
|
return
|
|
}
|
|
|
|
scores := r.calculator.BatchCalculate(observations, now)
|
|
|
|
if err := r.store.UpdateImportanceScores(ctx, scores); err != nil {
|
|
r.log.Error().Err(err).Msg("failed to update importance scores")
|
|
return
|
|
}
|
|
|
|
r.log.Info().
|
|
Int("count", len(scores)).
|
|
Dur("elapsed", time.Since(now)).
|
|
Msg("recalculated importance scores")
|
|
}
|
|
|
|
// RecalculateNow triggers an immediate recalculation.
|
|
// This is useful for testing or when scores need to be updated urgently.
|
|
func (r *Recalculator) RecalculateNow(ctx context.Context) error {
|
|
r.recalculate(ctx)
|
|
return nil
|
|
}
|
|
|
|
// RefreshConceptWeights reloads concept weights from the database.
|
|
// Call this after updating concept weights to apply changes.
|
|
func (r *Recalculator) RefreshConceptWeights(ctx context.Context) error {
|
|
weights, err := r.store.GetConceptWeights(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
config := r.calculator.GetConfig()
|
|
config.ConceptWeights = weights
|
|
r.calculator.UpdateConfig(config)
|
|
|
|
r.log.Info().Int("count", len(weights)).Msg("refreshed concept weights")
|
|
return nil
|
|
}
|
|
|
|
// Stats returns statistics about the recalculator.
|
|
type Stats struct {
|
|
Running bool `json:"running"`
|
|
Interval time.Duration `json:"interval"`
|
|
BatchSize int `json:"batch_size"`
|
|
HalfLife float64 `json:"half_life_days"`
|
|
MinScore float64 `json:"min_score"`
|
|
ConceptsLen int `json:"concepts_count"`
|
|
}
|
|
|
|
// GetStats returns current recalculator statistics.
|
|
func (r *Recalculator) GetStats() Stats {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
config := r.calculator.GetConfig()
|
|
|
|
return Stats{
|
|
Running: r.running,
|
|
Interval: r.interval,
|
|
BatchSize: r.batchSize,
|
|
HalfLife: config.RecencyHalfLifeDays,
|
|
MinScore: config.MinScore,
|
|
ConceptsLen: len(config.ConceptWeights),
|
|
}
|
|
}
|
|
|
|
// Ensure ObservationStore satisfies the interface
|
|
var _ ObservationStore = (*sqlite.ObservationStore)(nil)
|