Why: fullwidth text missed case-folded terms, SpecByID shared maps, the catalog prompt printed handlers and private metadata, overlapping slots kept both values, and negation was easy to mistake for semantics. Evidence: GOWORK=off go vet ./... && go test -count=1 -race ./... passed, including TestR2NFKCFoldsFullwidthButNotTraditional, TestR2SpecByIDDeepCopiesMetadata, TestR2CatalogHidesPrivateMetadataAndHandlerInMarkdown, TestR2EvalRulesDocumentsMaxSpecs, TestR2OverlappingSlotsAreAmbiguous, and TestR2NegationIsOnlyExcludesAny. Impact: NFKC is opt-in and does not convert traditional Chinese; catalog markdown omits handlers and non-allowlisted metadata; overlapping slot spans are invalid. Refs: I4, I7, I8, I10, I11, I12 Co-authored-by: Cursor <cursoragent@cursor.com>
211 lines
5.9 KiB
Go
211 lines
5.9 KiB
Go
package intent
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// SlotValidator 宿主注册的槽位值校验器。必须纯函数、确定性、无 I/O。
|
|
type SlotValidator func(value string) error
|
|
|
|
var slotValidators sync.Map // name → SlotValidator
|
|
|
|
// RegisterSlotValidator 注册进程级具名校验器;spec 通过 SlotSpec.Validator 引用。
|
|
// 必须在快照构建前注册(构建期会校验名字存在)。
|
|
//
|
|
// Deprecated: 校验器应随 Registry 隔离。请改用 Options.SlotValidators。
|
|
// 全局表只作回落:构建与校验先查作用域表,查不到再查这里。
|
|
func RegisterSlotValidator(name string, fn SlotValidator) {
|
|
slotValidators.Store(name, fn)
|
|
}
|
|
|
|
// LookupSlotValidator 按名查找已注册的全局校验器。
|
|
//
|
|
// Deprecated: 请改用 Options.SlotValidators。本函数只服务全局回落。
|
|
func LookupSlotValidator(name string) (SlotValidator, bool) {
|
|
fn, ok := slotValidators.Load(name)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
v, ok := fn.(SlotValidator)
|
|
return v, ok
|
|
}
|
|
|
|
// resolveSlotValidator 先查作用域表,查不到再回落已弃用的全局表。
|
|
func resolveSlotValidator(scoped map[string]SlotValidator, name string) (SlotValidator, bool) {
|
|
if fn, ok := scoped[name]; ok && fn != nil {
|
|
return fn, true
|
|
}
|
|
return LookupSlotValidator(name)
|
|
}
|
|
|
|
// 内置归一化步骤:trim / lower / upper。
|
|
var builtinNormalizers = map[string]func(string) string{
|
|
"trim": strings.TrimSpace,
|
|
"lower": strings.ToLower,
|
|
"upper": strings.ToUpper,
|
|
}
|
|
|
|
func normalizeSlotValue(v string, ops []string) string {
|
|
for _, op := range ops {
|
|
if fn, ok := builtinNormalizers[op]; ok {
|
|
v = fn(v)
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
// compiledSlot 编译后的槽位抽取与校验产物。
|
|
type compiledSlot struct {
|
|
key string
|
|
required bool
|
|
label string
|
|
question string
|
|
re *regexp.Regexp
|
|
group int
|
|
enumSet map[string]bool
|
|
enum []string
|
|
formatRe *regexp.Regexp
|
|
normalize []string
|
|
validator SlotValidator
|
|
}
|
|
|
|
// validate 对归一化后的值做确定性校验;返回(原因, 是否合法)。
|
|
func (s *compiledSlot) validate(v string) (string, bool) {
|
|
if s.formatRe != nil && !s.formatRe.MatchString(v) {
|
|
return "format mismatch: " + s.formatRe.String(), false
|
|
}
|
|
if s.enumSet != nil && !s.enumSet[v] {
|
|
return "value not in enum", false
|
|
}
|
|
if s.validator != nil {
|
|
if err := s.validator(v); err != nil {
|
|
return "validator: " + err.Error(), false
|
|
}
|
|
}
|
|
return "", true
|
|
}
|
|
|
|
// slotCapture 一次槽位抽取的捕获区间。多槽位各自第一次匹配;区间重叠时
|
|
// 两边都标 span overlap,不写入 slots。库内不做重叠消解。
|
|
type slotCapture struct {
|
|
key string
|
|
start int
|
|
end int
|
|
value string
|
|
ok bool
|
|
reason string
|
|
required bool
|
|
matched bool
|
|
}
|
|
|
|
// extractSlots 抽取并归一化槽位。
|
|
// missing:必填槽位未捕获(模式没匹配上);invalid:捕获到但校验不合法,或与另一槽位区间重叠(key→原因)。
|
|
// 必填且非法只进 invalid(宿主可区分"没给"与"给错了"来决定澄清话术)。
|
|
func (cs *compiledSpec) extractSlots(input string) (slots map[string]string, missing []string, invalid map[string]string) {
|
|
if len(cs.slots) == 0 {
|
|
return nil, nil, nil
|
|
}
|
|
caps := make([]slotCapture, 0, len(cs.slots))
|
|
for _, s := range cs.slots {
|
|
loc := s.re.FindStringSubmatchIndex(input)
|
|
g := s.group
|
|
if loc == nil || 2*g+1 >= len(loc) || loc[2*g] < 0 || loc[2*g] == loc[2*g+1] {
|
|
caps = append(caps, slotCapture{key: s.key, required: s.required})
|
|
continue
|
|
}
|
|
raw := input[loc[2*g]:loc[2*g+1]]
|
|
v := normalizeSlotValue(raw, s.normalize)
|
|
reason, ok := s.validate(v)
|
|
caps = append(caps, slotCapture{
|
|
key: s.key, start: loc[2*g], end: loc[2*g+1], value: v,
|
|
ok: ok, reason: reason, required: s.required, matched: true,
|
|
})
|
|
}
|
|
overlap := map[int]bool{}
|
|
for i := range caps {
|
|
if !caps[i].matched {
|
|
continue
|
|
}
|
|
for j := i + 1; j < len(caps); j++ {
|
|
if !caps[j].matched {
|
|
continue
|
|
}
|
|
if caps[i].start < caps[j].end && caps[j].start < caps[i].end {
|
|
overlap[i] = true
|
|
overlap[j] = true
|
|
}
|
|
}
|
|
}
|
|
for i, c := range caps {
|
|
if !c.matched {
|
|
if c.required {
|
|
missing = append(missing, c.key)
|
|
}
|
|
continue
|
|
}
|
|
if overlap[i] {
|
|
if invalid == nil {
|
|
invalid = map[string]string{}
|
|
}
|
|
invalid[c.key] = "span overlap"
|
|
continue
|
|
}
|
|
if !c.ok {
|
|
if invalid == nil {
|
|
invalid = map[string]string{}
|
|
}
|
|
invalid[c.key] = c.reason
|
|
continue
|
|
}
|
|
if slots == nil {
|
|
slots = map[string]string{}
|
|
}
|
|
slots[c.key] = c.value
|
|
}
|
|
return slots, missing, invalid
|
|
}
|
|
|
|
func compileSlot(spec SlotSpec, scoped map[string]SlotValidator) (compiledSlot, error) {
|
|
re, err := regexp.Compile(spec.Pattern)
|
|
if err != nil {
|
|
return compiledSlot{}, newError(InvalidSpec, "slot pattern 编译失败",
|
|
"field", "slots", "key", spec.Key, "error", err.Error())
|
|
}
|
|
cs := compiledSlot{
|
|
key: spec.Key,
|
|
required: spec.Required,
|
|
label: spec.Label,
|
|
question: spec.Question,
|
|
re: re,
|
|
group: effectiveGroup(spec.Group),
|
|
normalize: append([]string(nil), spec.Normalize...),
|
|
}
|
|
if len(spec.Enum) > 0 {
|
|
cs.enum = append([]string(nil), spec.Enum...)
|
|
cs.enumSet = make(map[string]bool, len(spec.Enum))
|
|
for _, e := range spec.Enum {
|
|
cs.enumSet[e] = true
|
|
}
|
|
}
|
|
if spec.Format != "" {
|
|
fre, err := regexp.Compile(spec.Format)
|
|
if err != nil {
|
|
return compiledSlot{}, newError(InvalidSpec, "slot format 编译失败",
|
|
"field", "slots", "key", spec.Key, "format", spec.Format, "error", err.Error())
|
|
}
|
|
cs.formatRe = fre
|
|
}
|
|
if spec.Validator != "" {
|
|
fn, ok := resolveSlotValidator(scoped, spec.Validator)
|
|
if !ok {
|
|
return compiledSlot{}, newError(InvalidSpec, "slot validator 未注册",
|
|
"field", "slots", "key", spec.Key, "validator", spec.Validator,
|
|
"hint", "在 Options.SlotValidators 登记,或调用已弃用的 intent.RegisterSlotValidator")
|
|
}
|
|
cs.validator = fn
|
|
}
|
|
return cs, nil
|
|
}
|