package evidence import ( "strings" "keel/internal/mode/focus/domain" ) // MatchesAllowed reports whether a window (class/title) is on-task per ctx. // M3 matches on window class and title only; the domain and command helpers are // ported for completeness and tests but have no live data source yet. func MatchesAllowed(ctx domain.AllowedContext, class, title string) bool { return windowClassAllowed(ctx, class) || windowTitleAllowed(ctx, title) } func windowClassAllowed(ctx domain.AllowedContext, candidate string) bool { candidate = normalizeCasefolded(candidate) if candidate == "" { return false } for _, allowed := range ctx.WindowClasses { // Substring, not equality: a short token a user types ("brave") matches the // longer real WM_CLASS ("Brave-browser"). Equality silently disabled the // fast path whenever the two differed, routing every window to the judge. if a := normalizeCasefolded(allowed); a != "" && strings.Contains(candidate, a) { return true } } return false } func windowTitleAllowed(ctx domain.AllowedContext, candidate string) bool { candidate = normalizeCasefolded(candidate) if candidate == "" { return false } for _, allowed := range ctx.WindowTitleSubstrings { a := normalizeCasefolded(allowed) if a != "" && strings.Contains(candidate, a) { return true } } return false } func domainAllowed(ctx domain.AllowedContext, candidate string) bool { candidate = normalizeDomain(candidate) if candidate == "" { return false } for _, allowed := range ctx.Domains { a := normalizeDomain(allowed) if a == "" { continue } if candidate == a { return true } if strings.HasSuffix(candidate, a) && strings.HasSuffix(candidate[:len(candidate)-len(a)], ".") { return true } } return false } func commandAllowed(ctx domain.AllowedContext, candidate string) bool { candidate = executableBasename(candidate) if candidate == "" { return false } for _, allowed := range ctx.Commands { if executableBasename(allowed) == candidate { return true } } return false } func normalizeDomain(v string) string { return strings.ToLower(strings.TrimRight(strings.TrimSpace(v), ".")) } func normalizeCasefolded(v string) string { return strings.ToLower(strings.TrimSpace(v)) } // executableBasename returns the lowercased basename of the first whitespace- // separated token (the executable), splitting on both / and \. func executableBasename(command string) string { fields := strings.Fields(command) if len(fields) == 0 { return "" } exe := fields[0] if i := strings.LastIndexAny(exe, `/\`); i >= 0 { exe = exe[i+1:] } return strings.ToLower(strings.TrimSpace(exe)) }