Port allowed-context matching to evidence package

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 16:42:48 -04:00
parent fccf856054
commit 7e9d00b80b
2 changed files with 171 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
package evidence
import (
"strings"
"antidrift/internal/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 {
if normalizeCasefolded(allowed) == candidate {
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))
}
+75
View File
@@ -0,0 +1,75 @@
package evidence
import (
"testing"
"antidrift/internal/domain"
)
func TestWindowClassMatchesCaseAndTrim(t *testing.T) {
ctx := domain.AllowedContext{WindowClasses: []string{" code "}}
if !windowClassAllowed(ctx, " Code ") {
t.Fatal("class should match case-insensitively after trim")
}
if windowClassAllowed(ctx, "firefox") {
t.Fatal("unrelated class must not match")
}
if windowClassAllowed(ctx, " ") {
t.Fatal("empty candidate must not match")
}
}
func TestWindowTitleMatchesSubstring(t *testing.T) {
ctx := domain.AllowedContext{WindowTitleSubstrings: []string{" antidrift "}}
if !windowTitleAllowed(ctx, "Commitment OS - AntiDrift") {
t.Fatal("title substring should match after trim/casefold")
}
if windowTitleAllowed(ctx, "random video") {
t.Fatal("unrelated title must not match")
}
}
func TestDomainMatchesExactAndSubdomain(t *testing.T) {
ctx := domain.AllowedContext{Domains: []string{" GitHub.COM. "}}
if !domainAllowed(ctx, "github.com") {
t.Fatal("exact domain should match")
}
if !domainAllowed(ctx, " DOCS.GITHUB.COM. ") {
t.Fatal("subdomain should match after normalization")
}
if domainAllowed(ctx, "evilgithub.com") {
t.Fatal("suffix-without-dot must not match")
}
}
func TestCommandMatchesExecutableBasename(t *testing.T) {
ctx := domain.AllowedContext{Commands: []string{"cargo"}}
if !commandAllowed(ctx, "/home/felixm/.cargo/bin/cargo test") {
t.Fatal("basename of full path should match")
}
if !commandAllowed(ctx, "cargo") {
t.Fatal("bare command should match")
}
if commandAllowed(ctx, "/usr/bin/git status") {
t.Fatal("unrelated command must not match")
}
}
func TestMatchesAllowedClassOrTitle(t *testing.T) {
ctx := domain.AllowedContext{
WindowClasses: []string{"code"},
WindowTitleSubstrings: []string{"antidrift"},
}
if !MatchesAllowed(ctx, "Code", "anything") {
t.Fatal("class match should satisfy MatchesAllowed")
}
if !MatchesAllowed(ctx, "firefox", "my AntiDrift tab") {
t.Fatal("title match should satisfy MatchesAllowed")
}
if MatchesAllowed(ctx, "firefox", "youtube") {
t.Fatal("neither match must be off-task")
}
if MatchesAllowed(domain.AllowedContext{}, "code", "antidrift") {
t.Fatal("empty context matches nothing")
}
}