package graph import ( "fmt" "net/url" "strings" "github.com/local/glpi-neural-brain/internal/model" ) // NodeFilter limits knowledge-bearing nodes by the exact value stored in the // KB document's source field. Values are compared case-sensitively after // trimming surrounding whitespace. An empty Sources list means unrestricted. // No category, origin, URI or taxonomy fallback participates in matching. type NodeFilter struct { Sources []string } func (f NodeFilter) Matches(node model.Node) bool { if len(f.Sources) == 0 { return true } source := strings.TrimSpace(NodeSource(node)) for _, wanted := range f.Sources { wanted = strings.TrimSpace(wanted) if wanted == "" { continue } if source == wanted { return true } } return false } // NodeSource returns only the explicit source metadata copied from the KB // file's top-level source field (or assigned explicitly to generated research // evidence). Technical origins such as glpi-kb are deliberately ignored. func NodeSource(node model.Node) string { return metadataText(node.Metadata, "source") } // SourceFromURL derives the explicit source value used for learned web // evidence. It is kept separate from NodeSource so ordinary nodes never fall // back to their URI implicitly. func SourceFromURL(raw string) string { parsed, err := url.Parse(strings.TrimSpace(raw)) if err != nil { return "" } return strings.ToLower(strings.TrimSpace(parsed.Hostname())) } func metadataText(metadata map[string]any, key string) string { if metadata == nil { return "" } value, ok := metadata[key] if !ok || value == nil { return "" } text := strings.TrimSpace(fmt.Sprint(value)) if text == "" || strings.EqualFold(text, "") || strings.EqualFold(text, "null") { return "" } return text }