// Package brain provides the wing/hall path taxonomy used by the brain // wiki layout. A note's canonical location is // brain/wiki///.md, where Wing is a free-form topic // domain and Hall is one of a closed vocabulary of memory types. package brain import ( "fmt" "path/filepath" "strings" ) // ValidHalls is the closed vocabulary of hall names. A hall captures the // memory type of a note within any wing. var ValidHalls = map[string]bool{ "facts": true, "decisions": true, "failures": true, "hypotheses": true, "sources": true, } // IsValidHall reports whether h is in the closed Hall vocabulary. func IsValidHall(h string) bool { return ValidHalls[h] } // NotePath resolves the canonical filesystem path for a note given a // wing, hall, and slug. Returns an error if hall is not in ValidHalls // or if wing/slug sanitise to empty strings. // // The returned path is brain/wiki///.md with all // segments sanitised: lowercased, alphanumerics and hyphens only. func NotePath(brainDir, wing, hall, slug string) (string, error) { if !IsValidHall(hall) { return "", fmt.Errorf("invalid hall %q: must be one of facts/decisions/failures/hypotheses/sources", hall) } w := Sanitise(wing) if w == "" { return "", fmt.Errorf("invalid wing %q: must contain at least one alphanumeric character", wing) } s := Sanitise(strings.TrimSuffix(slug, ".md")) if s == "" { return "", fmt.Errorf("invalid slug %q: must contain at least one alphanumeric character", slug) } return filepath.Join(brainDir, "wiki", w, hall, s+".md"), nil } // Sanitise lowercases s and keeps only [a-z0-9-], collapsing any other // character (including path separators) to a hyphen. Leading/trailing // hyphens and runs of hyphens are collapsed. func Sanitise(s string) string { s = strings.ToLower(strings.TrimSpace(s)) var b strings.Builder prevHyphen := true for _, r := range s { switch { case r >= 'a' && r <= 'z', r >= '0' && r <= '9': b.WriteRune(r) prevHyphen = false case r == '-' || r == '_' || r == ' ' || r == '/' || r == '\\' || r == '.': if !prevHyphen { b.WriteByte('-') prevHyphen = true } } } out := b.String() return strings.Trim(out, "-") }