Files
Agent-Intent/intent/fold.go
weichen 765a7e7177 Isolate snapshots and validate generated commands
Why: F07/F08 - registry and catalog accessors shared caller-mutable
nested state, and the generation fallback skipped full slot validation,
risk metadata and mis-keyed handler slot tables.
Evidence: intent package tests pass incl. new isolation/generated cases
(verify-u5.json).
Impact: no exported signature change; rule and generated paths now share
authoritative validation.
Refs: feature/agent-stack-audit-remediation-b1 U5
2026-10-21 15:41:31 +00:00

162 lines
3.8 KiB
Go

package intent
import (
"strings"
"golang.org/x/text/unicode/norm"
)
// foldMatchText 做识别用折叠。nfkc 只做兼容等价(全角 ASCII、组合字符),
// 不做繁简转换。caseSensitive 为真时保留大小写。
func foldMatchText(s string, nfkc, caseSensitive bool) string {
if nfkc {
s = norm.NFKC.String(s)
}
if !caseSensitive {
s = strings.ToLower(s)
}
return s
}
// foldInput 返回用于匹配的文本:第一段保留大小写(可选 NFKC),第二段是其小写。
func (s *Snapshot) foldInput(input string) (matchInput, folded string) {
if s == nil {
return input, strings.ToLower(input)
}
matchInput = foldMatchText(input, s.nfkc, true)
return matchInput, strings.ToLower(matchInput)
}
func cloneStrings(in []string) []string {
if in == nil {
return nil
}
return append([]string(nil), in...)
}
func cloneStringMap(in map[string]string) map[string]string {
if in == nil {
return nil
}
out := make(map[string]string, len(in))
for k, v := range in {
out[k] = v
}
return out
}
func cloneActionRef(in *ActionRef) *ActionRef {
if in == nil {
return nil
}
cp := *in
return &cp
}
// cloneIntentSpec 深拷贝 IntentSpec,切断切片/map/指针与调用方或快照内部的共享。
func cloneIntentSpec(in IntentSpec) IntentSpec {
out := in
out.Matcher.IncludesAny = cloneStrings(in.Matcher.IncludesAny)
out.Matcher.IncludesAll = cloneStrings(in.Matcher.IncludesAll)
out.Matcher.ExcludesAny = cloneStrings(in.Matcher.ExcludesAny)
out.Matcher.Patterns = cloneStrings(in.Matcher.Patterns)
if in.Slots != nil {
out.Slots = append([]SlotSpec(nil), in.Slots...)
for i := range out.Slots {
out.Slots[i].Enum = cloneStrings(in.Slots[i].Enum)
out.Slots[i].Normalize = cloneStrings(in.Slots[i].Normalize)
}
}
out.Metadata = cloneStringMap(in.Metadata)
out.Action = cloneActionRef(in.Action)
out.Examples = cloneStrings(in.Examples)
return out
}
func cloneCatalogSlot(in CatalogSlot) CatalogSlot {
out := in
out.Enum = cloneStrings(in.Enum)
return out
}
func cloneCatalogSpec(in CatalogSpec) CatalogSpec {
out := in
out.Metadata = cloneStringMap(in.Metadata)
if in.Slots != nil {
out.Slots = make([]CatalogSlot, len(in.Slots))
for i := range in.Slots {
out.Slots[i] = cloneCatalogSlot(in.Slots[i])
}
}
return out
}
func cloneCatalogSpecs(in []CatalogSpec) []CatalogSpec {
if in == nil {
return nil
}
out := make([]CatalogSpec, len(in))
for i := range in {
out[i] = cloneCatalogSpec(in[i])
}
return out
}
func cloneOptions(in Options) Options {
out := in
out.ProtectedDomains = cloneStrings(in.ProtectedDomains)
out.GuardDomains = cloneStrings(in.GuardDomains)
if in.SlotValidators != nil {
cp := make(map[string]SlotValidator, len(in.SlotValidators))
for k, v := range in.SlotValidators {
cp[k] = v
}
out.SlotValidators = cp
}
return out
}
func cloneCandidate(in Candidate) Candidate {
out := in
out.Metadata = cloneStringMap(in.Metadata)
out.Slots = cloneStringMap(in.Slots)
out.MissingRequiredSlots = cloneStrings(in.MissingRequiredSlots)
out.InvalidSlots = cloneStringMap(in.InvalidSlots)
out.Action = cloneActionRef(in.Action)
return out
}
func cloneCandidates(in []Candidate) []Candidate {
if in == nil {
return nil
}
out := make([]Candidate, len(in))
for i := range in {
out[i] = cloneCandidate(in[i])
}
return out
}
// catalogMetadataAllow 是目录与 Markdown 允许带出的 metadata 键。
// handler 仍留在 CatalogSpec 供执行器按名分派,但不写入 Markdown。
var catalogMetadataAllow = map[string]bool{
"blueprint": true,
"guidance": true,
}
func catalogMetadata(in map[string]string) map[string]string {
if len(in) == 0 {
return nil
}
out := map[string]string{}
for k, v := range in {
if catalogMetadataAllow[k] {
out[k] = v
}
}
if len(out) == 0 {
return nil
}
return out
}