Why: Refine could swap handlers or emit out-of-range scores, Ambiguous tops skipped slot extraction, and the process-global validator map coupled independent registries. Evidence: GOWORK=off go vet ./... && go test -count=1 ./... && go test -race ./... passed, including TestRecognizeClassifierRejectsBadRefinement, TestRecognizeClassifierRebuildsTamperedHandler, TestRecognizeClassifierRefinedTopGetsSlots, and TestScopedSlotValidatorsDoNotInterfere. error-code-system and logging-system advanced to a3ee67b and 49b5d8b (docs-only). Impact: illegal Refine results stay Ambiguous; a resolved Top is slotted; Options.SlotValidators wins over the deprecated global table. Refs: I1, I2, I6 Co-authored-by: Cursor <cursoragent@cursor.com>
162 lines
4.7 KiB
Go
162 lines
4.7 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
for _, s := range cs.slots {
|
|
m := s.re.FindStringSubmatch(input)
|
|
if m == nil || s.group >= len(m) || m[s.group] == "" {
|
|
if s.required {
|
|
missing = append(missing, s.key)
|
|
}
|
|
continue
|
|
}
|
|
v := normalizeSlotValue(m[s.group], s.normalize)
|
|
if reason, ok := s.validate(v); !ok {
|
|
if invalid == nil {
|
|
invalid = map[string]string{}
|
|
}
|
|
invalid[s.key] = reason
|
|
continue
|
|
}
|
|
if slots == nil {
|
|
slots = map[string]string{}
|
|
}
|
|
slots[s.key] = v
|
|
}
|
|
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
|
|
}
|