package services import ( annotationrules "pvfine/internal/annotations" "pvfine/internal/buildmode" ) type AnnotationReloadResult struct { RuleCount int `json:"ruleCount"` RelationCount int `json:"relationCount"` } type AnnotationService struct { c *core path string initErr error layered bool } func NewAnnotationService(c *core) *AnnotationService { if c.ruleStore != nil { return &AnnotationService{c: c, layered: true} } if path, ok := annotationrules.FindSourcePath(); buildmode.Development && ok { return &AnnotationService{c: c, path: path} } path, err := annotationrules.RuntimePath() if err != nil { return &AnnotationService{c: c, initErr: err} } return &AnnotationService{c: c, path: path} } func newAnnotationService(c *core, path string) *AnnotationService { return &AnnotationService{c: c, path: path} } // ReloadRules validates the on-disk rules before atomically replacing the // active engine. Invalid files leave the current engine untouched. func (s *AnnotationService) ReloadRules() (AnnotationReloadResult, error) { if s.layered { if err := NewRuleService(s.c).reload("annotations"); err != nil { return AnnotationReloadResult{}, err } s.c.mu.RLock() defer s.c.mu.RUnlock() document := s.c.annotationEngine.Document() return AnnotationReloadResult{RuleCount: len(document.Rules), RelationCount: len(document.Relations)}, nil } if s.initErr != nil { return AnnotationReloadResult{}, s.initErr } engine, err := annotationrules.LoadFile(s.path) if err != nil { return AnnotationReloadResult{}, err } document := engine.Document() s.c.mu.Lock() oldSpecsFingerprint := searchIndexSpecFingerprint(s.c.searchableListSpecsLocked()) engine = engine.ForVersion(s.c.archive.ClientVersion()) s.c.annotationEngine = engine s.c.annotationSnapshot = nil s.c.annotationErr = nil s.c.annotationRelations = make(map[string]map[string]*relationTarget) s.c.editorAnnotation = editorAnnotationCache{} archiveOpen := s.c.archive != nil if archiveOpen { s.c.pathAnnotations = buildPathAnnotations(engine, s.c.dirChildren) } newSpecsFingerprint := searchIndexSpecFingerprint(s.c.searchableListSpecsLocked()) if archiveOpen && oldSpecsFingerprint != newSpecsFingerprint { s.c.markAdvancedSearchStaleLocked(s.c.archive) } if archiveOpen && oldSpecsFingerprint != newSpecsFingerprint && s.c.diskIndex != nil { s.c.diskIndex.dirty = true s.c.diskIndex.semanticEpoch++ } s.c.mu.Unlock() if archiveOpen && oldSpecsFingerprint != newSpecsFingerprint { s.c.startSearchIndex() } result := AnnotationReloadResult{ RuleCount: len(document.Rules), RelationCount: len(document.Relations), } emitEvent("annotations:reloaded", result) return result, nil }