Files
claude-mnemonic/pkg/models/conflict.go
lukaszraczylo 4f4b4ac70f feat(chunking): add AST-aware code chunking for Go, Python, TypeScript
- [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
2026-01-07 13:19:58 +00:00

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
}