Files
weichen fa512ec03c
ci / vet-and-test (pull_request) Successful in 17s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Add command model, priority tie-break, and boundary matching
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
2026-10-07 07:21:55 +00:00

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 == '_'
}