Why: I1 increment one - recognition output becomes a command list for the host router, same-score ties resolve by priority (I5), and CJK substring false positives get an opt-in boundary mode (I3). Evidence: go vet clean; go test ./... all ok, including new command (6), boundary (3), and tie-break tests; intentctl lint smoke run shows boundary hints on short unbounded terms. Existing classifier and ambiguity fixtures repointed to an equal-priority pair; the greet corpus case now expects the I5 tie-break outcome. Impact: Result gains Commands; recognizer derives them at one point (guard/fallback stay empty); MatchSpec gains Boundary (default off, legacy behavior unchanged); lint gains boundary hints. Top/Branch stay for compatibility. Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md I1 WP1/WP4/WP5
63 lines
1.8 KiB
Go
63 lines
1.8 KiB
Go
package intent
|
|
|
|
import (
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// boundedContains 词界匹配。规则分两边:
|
|
// - 右缘(必须):命中的右侧不得与词条末字符同类——汉字对汉字、词字符对词字符。
|
|
// 「配置」不命中「配置项」(右邻「项」为汉字)、「write」不命中「rewrites」。
|
|
// - 左缘(仅拉丁词条):汉字结尾的词条不要求左界——中文没有天然词界,
|
|
// 「改配置」「系统配置」里的「配置」仍是用户想说那个词;
|
|
// 拉丁词条按经典 \b 要求两侧(rewrites 两侧的 write 都不算命中)。
|
|
func boundedContains(haystack, term string) bool {
|
|
if term == "" {
|
|
return false
|
|
}
|
|
first, _ := utf8.DecodeRuneInString(term)
|
|
last, _ := utf8.DecodeLastRuneInString(term)
|
|
checkLeft := isWord(first) && !isHan(first) // 仅拉丁开头的词条要求左界
|
|
for i := strings.Index(haystack, term); i >= 0; {
|
|
end := i + len(term)
|
|
if (!checkLeft || leftBounded(haystack, i, first)) && rightBounded(haystack, end, last) {
|
|
return true
|
|
}
|
|
j := strings.Index(haystack[i+1:], term)
|
|
if j < 0 {
|
|
return false
|
|
}
|
|
i += 1 + j
|
|
}
|
|
return false
|
|
}
|
|
|
|
func leftBounded(s string, i int, termFirst rune) bool {
|
|
if i == 0 {
|
|
return true
|
|
}
|
|
prev, _ := utf8.DecodeLastRuneInString(s[:i])
|
|
return !sameClass(prev, termFirst)
|
|
}
|
|
|
|
func rightBounded(s string, end int, termLast rune) bool {
|
|
if end >= len(s) {
|
|
return true
|
|
}
|
|
next, _ := utf8.DecodeRuneInString(s[end:])
|
|
return !sameClass(next, termLast)
|
|
}
|
|
|
|
func sameClass(a, b rune) bool {
|
|
return (isHan(a) && isHan(b)) || (isWord(a) && isWord(b))
|
|
}
|
|
|
|
func isHan(r rune) bool {
|
|
return unicode.Is(unicode.Han, r)
|
|
}
|
|
|
|
func isWord(r rune) bool {
|
|
return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_'
|
|
}
|