mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-06 23:13:50 +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
259 lines
8.7 KiB
Go
259 lines
8.7 KiB
Go
// Package models contains domain models for claude-mnemonic.
|
|
package models
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ConflictType represents the type of conflict between observations.
|
|
type ConflictType string
|
|
|
|
const (
|
|
// ConflictSuperseded means newer observation supersedes older one (same topic, updated info).
|
|
ConflictSuperseded ConflictType = "superseded"
|
|
// ConflictContradicts means observations contain contradictory information.
|
|
ConflictContradicts ConflictType = "contradicts"
|
|
// ConflictOutdatedPattern means an outdated pattern/practice was identified.
|
|
ConflictOutdatedPattern ConflictType = "outdated_pattern"
|
|
)
|
|
|
|
// ConflictResolution indicates which observation to prefer.
|
|
type ConflictResolution string
|
|
|
|
const (
|
|
// ResolutionPreferNewer means prefer the newer observation.
|
|
ResolutionPreferNewer ConflictResolution = "prefer_newer"
|
|
// ResolutionPreferOlder means prefer the older observation (rare).
|
|
ResolutionPreferOlder ConflictResolution = "prefer_older"
|
|
// ResolutionManual means manual review is needed.
|
|
ResolutionManual ConflictResolution = "manual"
|
|
)
|
|
|
|
// ObservationConflict tracks conflicting observations.
|
|
type ObservationConflict struct {
|
|
ResolvedAt *string `db:"resolved_at" json:"resolved_at,omitempty"`
|
|
ConflictType ConflictType `db:"conflict_type" json:"conflict_type"`
|
|
Resolution ConflictResolution `db:"resolution" json:"resolution"`
|
|
Reason string `db:"reason" json:"reason"`
|
|
DetectedAt string `db:"detected_at" json:"detected_at"`
|
|
ID int64 `db:"id" json:"id"`
|
|
NewerObsID int64 `db:"newer_obs_id" json:"newer_obs_id"`
|
|
OlderObsID int64 `db:"older_obs_id" json:"older_obs_id"`
|
|
DetectedAtEpoch int64 `db:"detected_at_epoch" json:"detected_at_epoch"`
|
|
Resolved bool `db:"resolved" json:"resolved"`
|
|
}
|
|
|
|
// ConflictDetectionResult contains the result of conflict detection.
|
|
type ConflictDetectionResult struct {
|
|
Type ConflictType
|
|
Resolution ConflictResolution
|
|
Reason string
|
|
OlderObsIDs []int64
|
|
HasConflict bool
|
|
}
|
|
|
|
// NewObservationConflict creates a new conflict record.
|
|
func NewObservationConflict(newerID, olderID int64, conflictType ConflictType, resolution ConflictResolution, reason string) *ObservationConflict {
|
|
now := time.Now()
|
|
return &ObservationConflict{
|
|
NewerObsID: newerID,
|
|
OlderObsID: olderID,
|
|
ConflictType: conflictType,
|
|
Resolution: resolution,
|
|
Reason: reason,
|
|
DetectedAt: now.Format(time.RFC3339),
|
|
DetectedAtEpoch: now.UnixMilli(),
|
|
Resolved: false,
|
|
}
|
|
}
|
|
|
|
// CorrectionPatterns contains regex patterns that indicate explicit corrections.
|
|
var CorrectionPatterns = []*regexp.Regexp{
|
|
regexp.MustCompile(`(?i)\bactually[,\s]+that\s+was\s+wrong\b`),
|
|
regexp.MustCompile(`(?i)\bactually[,\s]+that's\s+(wrong|incorrect|not\s+right)\b`),
|
|
regexp.MustCompile(`(?i)\bpreviously\s+(said|mentioned|noted)\s+.*\s+but\b`),
|
|
regexp.MustCompile(`(?i)\bcorrection:\s*`),
|
|
regexp.MustCompile(`(?i)\bignore\s+(the\s+)?(previous|earlier)\b`),
|
|
regexp.MustCompile(`(?i)\bdisregard\s+(the\s+)?(previous|earlier)\b`),
|
|
regexp.MustCompile(`(?i)\bwas\s+(wrong|incorrect|mistaken)\b`),
|
|
regexp.MustCompile(`(?i)\bturns\s+out\s+.*(wrong|incorrect|not\s+the\s+case)\b`),
|
|
regexp.MustCompile(`(?i)\b(supersedes|replaces|overrides)\s+(the\s+)?(previous|earlier|old)\b`),
|
|
regexp.MustCompile(`(?i)\b(don't|do\s+not)\s+use\s+.*\s+anymore\b`),
|
|
regexp.MustCompile(`(?i)\bno\s+longer\s+(valid|applicable|correct|recommended)\b`),
|
|
regexp.MustCompile(`(?i)\bdeprecated\s+(approach|method|pattern|way)\b`),
|
|
regexp.MustCompile(`(?i)\bshould\s+have\s+(been|used)\b.*instead\b`),
|
|
regexp.MustCompile(`(?i)\bbetter\s+(approach|way|method|solution)\s+is\b`),
|
|
}
|
|
|
|
// OpposingChangePatterns detects add/remove conflicts.
|
|
var OpposingChangePatterns = map[string]string{
|
|
"add": "remove",
|
|
"added": "removed",
|
|
"create": "delete",
|
|
"created": "deleted",
|
|
"enable": "disable",
|
|
"enabled": "disabled",
|
|
"include": "exclude",
|
|
"allow": "deny",
|
|
"permit": "block",
|
|
}
|
|
|
|
// DetectExplicitCorrection checks if text contains explicit correction language.
|
|
func DetectExplicitCorrection(text string) (bool, string) {
|
|
for _, pattern := range CorrectionPatterns {
|
|
if match := pattern.FindString(text); match != "" {
|
|
return true, "Explicit correction detected: " + match
|
|
}
|
|
}
|
|
return false, ""
|
|
}
|
|
|
|
// DetectOpposingFileChanges checks if two observations have opposing changes on the same file.
|
|
func DetectOpposingFileChanges(newer, older *Observation) (bool, string) {
|
|
// Check for overlapping modified files
|
|
newerFiles := make(map[string]bool)
|
|
for _, f := range newer.FilesModified {
|
|
newerFiles[f] = true
|
|
}
|
|
|
|
var overlappingFiles []string
|
|
for _, f := range older.FilesModified {
|
|
if newerFiles[f] {
|
|
overlappingFiles = append(overlappingFiles, f)
|
|
}
|
|
}
|
|
|
|
if len(overlappingFiles) == 0 {
|
|
return false, ""
|
|
}
|
|
|
|
// Check for opposing action words in titles/narratives
|
|
newerText := strings.ToLower(newer.Title.String + " " + newer.Narrative.String)
|
|
olderText := strings.ToLower(older.Title.String + " " + older.Narrative.String)
|
|
|
|
for action, opposite := range OpposingChangePatterns {
|
|
if (strings.Contains(newerText, action) && strings.Contains(olderText, opposite)) ||
|
|
(strings.Contains(newerText, opposite) && strings.Contains(olderText, action)) {
|
|
return true, "Opposing changes on files: " + strings.Join(overlappingFiles, ", ")
|
|
}
|
|
}
|
|
|
|
return false, ""
|
|
}
|
|
|
|
// DetectConceptTagMismatch checks if observations have same concepts but different recommendations.
|
|
func DetectConceptTagMismatch(newer, older *Observation) (bool, string) {
|
|
// Find overlapping concepts
|
|
newerConcepts := make(map[string]bool)
|
|
for _, c := range newer.Concepts {
|
|
newerConcepts[c] = true
|
|
}
|
|
|
|
var overlapping []string
|
|
for _, c := range older.Concepts {
|
|
if newerConcepts[c] {
|
|
overlapping = append(overlapping, c)
|
|
}
|
|
}
|
|
|
|
if len(overlapping) == 0 {
|
|
return false, ""
|
|
}
|
|
|
|
// Check if same file was modified and concepts overlap
|
|
// This suggests the newer observation may update the approach
|
|
newerFiles := make(map[string]bool)
|
|
for _, f := range newer.FilesModified {
|
|
newerFiles[f] = true
|
|
}
|
|
for _, f := range older.FilesModified {
|
|
if newerFiles[f] {
|
|
// Same file modified with same concepts - likely an update
|
|
return true, "Same concepts (" + strings.Join(overlapping, ", ") + ") with overlapping file changes"
|
|
}
|
|
}
|
|
|
|
return false, ""
|
|
}
|
|
|
|
// DetectConflict performs comprehensive conflict detection between a new observation
|
|
// and an existing one. Returns detection result.
|
|
func DetectConflict(newer, older *Observation) *ConflictDetectionResult {
|
|
result := &ConflictDetectionResult{
|
|
HasConflict: false,
|
|
}
|
|
|
|
// 1. Check for explicit correction language in newer observation
|
|
if newer.Narrative.Valid {
|
|
if isCorrection, reason := DetectExplicitCorrection(newer.Narrative.String); isCorrection {
|
|
result.HasConflict = true
|
|
result.Type = ConflictContradicts
|
|
result.Resolution = ResolutionPreferNewer
|
|
result.Reason = reason
|
|
result.OlderObsIDs = append(result.OlderObsIDs, older.ID)
|
|
return result
|
|
}
|
|
}
|
|
|
|
// Check title as well
|
|
if newer.Title.Valid {
|
|
if isCorrection, reason := DetectExplicitCorrection(newer.Title.String); isCorrection {
|
|
result.HasConflict = true
|
|
result.Type = ConflictContradicts
|
|
result.Resolution = ResolutionPreferNewer
|
|
result.Reason = reason
|
|
result.OlderObsIDs = append(result.OlderObsIDs, older.ID)
|
|
return result
|
|
}
|
|
}
|
|
|
|
// 2. Check for opposing file changes
|
|
if isOpposing, reason := DetectOpposingFileChanges(newer, older); isOpposing {
|
|
result.HasConflict = true
|
|
result.Type = ConflictSuperseded
|
|
result.Resolution = ResolutionPreferNewer
|
|
result.Reason = reason
|
|
result.OlderObsIDs = append(result.OlderObsIDs, older.ID)
|
|
return result
|
|
}
|
|
|
|
// 3. Check for concept tag mismatches with same files
|
|
if isMismatch, reason := DetectConceptTagMismatch(newer, older); isMismatch {
|
|
result.HasConflict = true
|
|
result.Type = ConflictSuperseded
|
|
result.Resolution = ResolutionPreferNewer
|
|
result.Reason = reason
|
|
result.OlderObsIDs = append(result.OlderObsIDs, older.ID)
|
|
return result
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// DetectConflictsWithExisting checks a new observation against a list of existing observations.
|
|
// Returns all detected conflicts.
|
|
func DetectConflictsWithExisting(newer *Observation, existing []*Observation) []*ConflictDetectionResult {
|
|
var results []*ConflictDetectionResult
|
|
|
|
for _, older := range existing {
|
|
// Skip self-comparison
|
|
if older.ID == newer.ID {
|
|
continue
|
|
}
|
|
|
|
// Only compare within same project (or both global)
|
|
if newer.Project != older.Project && newer.Scope != ScopeGlobal && older.Scope != ScopeGlobal {
|
|
continue
|
|
}
|
|
|
|
result := DetectConflict(newer, older)
|
|
if result.HasConflict {
|
|
results = append(results, result)
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|