Ctrl K
ComunidadGuías…

agent/filefinder/text.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package filefinder

import "slices"

func toLowerASCII(b byte) byte {
	if b >= 'A' && b <= 'Z' {
		return b + ('a' - 'A')
	}
	return b
}

func normalizeQuery(q string) string {
	b := make([]byte, 0, len(q))
	prevSpace := true
	for i := 0; i < len(q); i++ {
		c := q[i]
		if c == '\\' {
			c = '/'
		}
		c = toLowerASCII(c)
		if c == ' ' {
			if prevSpace {
				continue
			}
			prevSpace = true
		} else {
			prevSpace = false
		}
		b = append(b, c)
	}
	if len(b) > 0 && b[len(b)-1] == ' ' {
		b = b[:len(b)-1]
	}
	return string(b)
}

func normalizePathBytes(p []byte) []byte {
	j := 0
	prevSlash := false
	for i := 0; i < len(p); i++ {
		c := p[i]
		if c == '\\' {
			c = '/'
		}
		c = toLowerASCII(c)
		if c == '/' {
			if prevSlash {
				continue
			}
			prevSlash = true
		} else {
			prevSlash = false
		}
		p[j] = c
		j++
	}
	return p[:j]
}

// extractTrigrams returns deduplicated, sorted trigrams (three-byte
// subsequences) from s. Trigrams are the primary index key: a
// document matches a query only if every query trigram appears in
// the document, giving O(1) candidate filtering per trigram.
func extractTrigrams(s []byte) []uint32 {
	if len(s) < 3 {
		return nil
	}
	seen := make(map[uint32]struct{}, len(s))
	for i := 0; i <= len(s)-3; i++ {
		b0 := toLowerASCII(s[i])
		b1 := toLowerASCII(s[i+1])
		b2 := toLowerASCII(s[i+2])
		gram := uint32(b0)<<16 | uint32(b1)<<8 | uint32(b2)
		seen[gram] = struct{}{}
	}
	result := make([]uint32, 0, len(seen))
	for g := range seen {
		result = append(result, g)
	}
	slices.Sort(result)
	return result
}

func extractBasename(path []byte) (offset int, length int) {
	end := len(path)
	if end > 0 && path[end-1] == '/' {
		end--
	}
	if end == 0 {
		return 0, 0
	}
	i := end - 1
	for i >= 0 && path[i] != '/' {
		i--
	}
	start := i + 1
	return start, end - start
}

func extractSegments(path []byte) [][]byte {
	var segments [][]byte
	start := 0
	for i := 0; i <= len(path); i++ {
		if i == len(path) || path[i] == '/' {
			if i > start {
				segments = append(segments, path[start:i])
			}
			start = i + 1
		}
	}
	return segments
}

func prefix1(name []byte) byte {
	if len(name) == 0 {
		return 0
	}
	return toLowerASCII(name[0])
}

func prefix2(name []byte) uint16 {
	if len(name) == 0 {
		return 0
	}
	hi := uint16(toLowerASCII(name[0])) << 8
	if len(name) < 2 {
		return hi
	}
	return hi | uint16(toLowerASCII(name[1]))
}

// scoreParams controls the weights for each scoring signal.
type scoreParams struct {
	BasenameMatch  float32
	BasenamePrefix float32
	ExactSegment   float32
	BoundaryHit    float32
	ContiguousRun  float32
	DirTokenHit    float32
	DepthPenalty   float32
	LengthPenalty  float32
}

func defaultScoreParams() scoreParams {
	return scoreParams{
		BasenameMatch:  6.0,
		BasenamePrefix: 3.5,
		ExactSegment:   2.5,
		BoundaryHit:    1.8,
		ContiguousRun:  1.2,
		DirTokenHit:    0.4,
		DepthPenalty:   0.08,
		LengthPenalty:  0.01,
	}
}

func isSubsequence(haystack, needle []byte) bool {
	if len(needle) == 0 {
		return true
	}
	ni := 0
	for _, hb := range haystack {
		if toLowerASCII(hb) == toLowerASCII(needle[ni]) {
			ni++
			if ni == len(needle) {
				return true
			}
		}
	}
	return false
}

func longestContiguousMatch(haystack, needle []byte) int {
	if len(needle) == 0 || len(haystack) == 0 {
		return 0
	}
	best := 0
	ni := 0
	run := 0
	for _, hb := range haystack {
		if ni < len(needle) && toLowerASCII(hb) == toLowerASCII(needle[ni]) {
			run++
			ni++
			if run > best {
				best = run
			}
		} else {
			run = 0
			ni = 0
			if ni < len(needle) && toLowerASCII(hb) == toLowerASCII(needle[ni]) {
				run = 1
				ni = 1
				if run > best {
					best = run
				}
			}
		}
	}
	return best
}

func isBoundary(b byte) bool {
	return b == '/' || b == '.' || b == '_' || b == '-'
}

func countBoundaryHits(path []byte, query []byte) int {
	if len(query) == 0 || len(path) == 0 {
		return 0
	}
	hits := 0
	qi := 0
	for pi := 0; pi < len(path) && qi < len(query); pi++ {
		atBoundary := pi == 0 || isBoundary(path[pi-1])
		if atBoundary && toLowerASCII(path[pi]) == toLowerASCII(query[qi]) {
			hits++
			qi++
		}
	}
	return hits
}

func equalFoldASCII(a, b []byte) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if toLowerASCII(a[i]) != toLowerASCII(b[i]) {
			return false
		}
	}
	return true
}

func hasPrefixFoldASCII(haystack, prefix []byte) bool {
	if len(prefix) > len(haystack) {
		return false
	}
	for i := range prefix {
		if toLowerASCII(haystack[i]) != toLowerASCII(prefix[i]) {
			return false
		}
	}
	return true
}

// scorePath computes a relevance score for a candidate path
// against a query. The score combines several signals:
// basename match, basename prefix, exact segment match,
// word-boundary hits, longest contiguous run, and penalties
// for depth and length. A return value of 0 means no match
// (the query is not a subsequence of the path).
func scorePath(
	path []byte,
	baseOff int,
	baseLen int,
	depth int,
	query []byte,
	queryTokens [][]byte,
	params scoreParams,
) float32 {
	if !isSubsequence(path, query) {
		return 0
	}
	var score float32
	basename := path[baseOff : baseOff+baseLen]
	if isSubsequence(basename, query) {
		score += params.BasenameMatch
	}
	if hasPrefixFoldASCII(basename, query) {
		score += params.BasenamePrefix
	}
	segments := extractSegments(path)
	for _, token := range queryTokens {
		for _, seg := range segments {
			if equalFoldASCII(seg, token) {
				score += params.ExactSegment
				break
			}
		}
	}
	bh := countBoundaryHits(path, query)
	score += float32(bh) * params.BoundaryHit
	lcm := longestContiguousMatch(path, query)
	score += float32(lcm) * params.ContiguousRun
	score -= float32(depth) * params.DepthPenalty
	score -= float32(len(path)) * params.LengthPenalty
	return score
}