Why: I2 layering - between rules and the L2 classifier there is now a deterministic host-scored semantic layer (rules then semantic then model), and fallback can be rescued by a validated command generator instead of going straight to the default channel. Evidence: go vet clean; full suite ok, including ten new tests (rerank resolve/below-threshold/tie/error-then-L2, generator valid/invalid/error, catalog top-k). Impact: IntentSpec gains Examples; Options gains Semantic (with threshold), Generator; ambiguous candidates rerank through host scores before L2; fallback validates generated commands (action/slots/kind fail-closed) and synthesizes Top with slots; generated commands are no longer overwritten by deriveCommands; CatalogTopK by priority. Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md I2
680 lines
19 KiB
Go
680 lines
19 KiB
Go
package intent
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"io/fs"
|
||
"math"
|
||
"regexp"
|
||
"sort"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
|
||
"logging-system/logsys"
|
||
)
|
||
|
||
// Options 注册表与识别器的共享选项。零值字段在构建时填入默认值;
|
||
// GuardDomains / ProtectedDomains 决定快照的阶段归属与 overlay 保护范围。
|
||
type Options struct {
|
||
// ProtectedDomains 保护域:overlay 覆盖或禁用这些域内的 spec 会整批失败(fail-closed)。
|
||
ProtectedDomains []string
|
||
// GuardDomains 守卫域:这些域内的 spec 在 Stage 0 演练,命中即短路返回 GuardHit。
|
||
GuardDomains []string
|
||
// TopN 结果候选数上限(默认 5)。
|
||
TopN int
|
||
// AmbiguityMargin 前两名分差小于该值判 Ambiguous(默认 0.1)。
|
||
AmbiguityMargin float64
|
||
// MaxInputLen 输入字节长度上限(默认 4096),超过返回 InvalidArgument。
|
||
MaxInputLen int
|
||
// MaxSpecs 快照内 spec 总数上限,含禁用(默认 2048)。
|
||
MaxSpecs int
|
||
// MaxPatternsPerSpec 单 spec 正则数量上限(默认 32)。
|
||
MaxPatternsPerSpec int
|
||
// MaxSlotsPerSpec 单 spec 槽位数量上限(默认 16)。
|
||
MaxSlotsPerSpec int
|
||
// Log 可选日志器;nil 时使用 Nop。识别日志不记录原始输入。
|
||
Log *logsys.Logger
|
||
// Semantic 语义相似度端口(I2):nil 时语义层不启用。
|
||
Semantic SemanticScorer
|
||
// SemanticThreshold 语义重排的过阈值(默认 0.8)。
|
||
SemanticThreshold float64
|
||
// Generator 模型命令生成端口(I2):nil 时 fallback 不生成。
|
||
Generator CommandGenerator
|
||
// SlotValidators 作用域槽位校验器。构建与校验时先查此表,查不到再回落
|
||
// 已弃用的全局表(RegisterSlotValidator)。不同 Registry 的同名校验器互不影响。
|
||
SlotValidators map[string]SlotValidator
|
||
// NFKC 为真时,词条与输入先做兼容等价归一(全角 ASCII、组合字符),再按
|
||
// case_sensitive 决定是否小写。NFKC 不做繁简转换。
|
||
NFKC bool
|
||
}
|
||
|
||
// Options 字段默认值。
|
||
const (
|
||
DefaultTopN = 5
|
||
DefaultAmbiguityMargin = 0.1
|
||
DefaultMaxInputLen = 4096
|
||
DefaultMaxSpecs = 2048
|
||
DefaultMaxPatterns = 32
|
||
DefaultMaxSlots = 16
|
||
)
|
||
|
||
func (o Options) normalized() Options {
|
||
n := o
|
||
if n.TopN <= 0 {
|
||
n.TopN = DefaultTopN
|
||
}
|
||
if n.AmbiguityMargin <= 0 {
|
||
n.AmbiguityMargin = DefaultAmbiguityMargin
|
||
}
|
||
if n.SemanticThreshold <= 0 {
|
||
n.SemanticThreshold = DefaultSemanticThreshold
|
||
}
|
||
if n.MaxInputLen <= 0 {
|
||
n.MaxInputLen = DefaultMaxInputLen
|
||
}
|
||
if n.MaxSpecs <= 0 {
|
||
n.MaxSpecs = DefaultMaxSpecs
|
||
}
|
||
if n.MaxPatternsPerSpec <= 0 {
|
||
n.MaxPatternsPerSpec = DefaultMaxPatterns
|
||
}
|
||
if n.MaxSlotsPerSpec <= 0 {
|
||
n.MaxSlotsPerSpec = DefaultMaxSlots
|
||
}
|
||
if n.Log == nil {
|
||
n.Log = logsys.Nop()
|
||
}
|
||
if len(n.SlotValidators) > 0 {
|
||
cp := make(map[string]SlotValidator, len(n.SlotValidators))
|
||
for k, v := range n.SlotValidators {
|
||
cp[k] = v
|
||
}
|
||
n.SlotValidators = cp
|
||
}
|
||
return n
|
||
}
|
||
|
||
func (o Options) protectedSet() map[string]bool {
|
||
return domainSet(o.ProtectedDomains)
|
||
}
|
||
|
||
func (o Options) guardSet() map[string]bool {
|
||
return domainSet(o.GuardDomains)
|
||
}
|
||
|
||
func domainSet(domains []string) map[string]bool {
|
||
m := make(map[string]bool, len(domains))
|
||
for _, d := range domains {
|
||
m[d] = true
|
||
}
|
||
return m
|
||
}
|
||
|
||
func (o Options) limits() limits {
|
||
return limits{maxSpecs: o.MaxSpecs, maxPatternsPerSpec: o.MaxPatternsPerSpec, maxSlotsPerSpec: o.MaxSlotsPerSpec}
|
||
}
|
||
|
||
// ---- 编译产物 ----
|
||
// 槽位的编译产物与抽取/校验逻辑见 slots.go。
|
||
|
||
type compiledSpec struct {
|
||
spec IntentSpec
|
||
termsAny []string // 已按 case_sensitive 折叠
|
||
termsAll []string
|
||
termsEx []string
|
||
patterns []*regexp.Regexp
|
||
slots []compiledSlot
|
||
guard bool
|
||
index int // 合并序,确定性次序裁决用
|
||
base float64
|
||
bonus float64
|
||
threshold float64
|
||
}
|
||
|
||
// matchOutcome 单个 spec 对一次输入的评估结果。
|
||
type matchOutcome struct {
|
||
score float64
|
||
passed bool
|
||
hitTerms []string
|
||
hitPat []string
|
||
reject string // 非 passed 时的排除原因
|
||
}
|
||
|
||
// evaluate 按文档化的计分规则评估。input 为原文,folded 为小写折叠后文本。
|
||
func (cs *compiledSpec) evaluate(input, folded string) matchOutcome {
|
||
hay := input
|
||
if !cs.spec.Matcher.CaseSensitive {
|
||
hay = folded
|
||
}
|
||
for _, t := range cs.termsEx {
|
||
if cs.contains(hay, t) {
|
||
return matchOutcome{reject: "excluded_term:" + t}
|
||
}
|
||
}
|
||
for _, t := range cs.termsAll {
|
||
if !cs.contains(hay, t) {
|
||
return matchOutcome{reject: "missing_includes_all:" + t}
|
||
}
|
||
}
|
||
var hitTerms, hitPat []string
|
||
positive := false
|
||
for _, t := range cs.termsAny {
|
||
if cs.contains(hay, t) {
|
||
positive = true
|
||
hitTerms = append(hitTerms, t)
|
||
}
|
||
}
|
||
for i, p := range cs.patterns {
|
||
if p.MatchString(input) {
|
||
positive = true
|
||
hitPat = append(hitPat, cs.spec.Matcher.Patterns[i])
|
||
}
|
||
}
|
||
if !positive && (len(cs.termsAny) > 0 || len(cs.patterns) > 0) {
|
||
return matchOutcome{reject: "no_positive_match"}
|
||
}
|
||
score := cs.base
|
||
if len(hitPat) > 0 {
|
||
score += cs.bonus
|
||
}
|
||
if score > 1 {
|
||
score = 1
|
||
}
|
||
// 固定精度舍入:避免浮点噪声进入结果与 margin 比较。
|
||
return matchOutcome{score: math.Round(score*10000) / 10000, passed: score >= cs.threshold, hitTerms: hitTerms, hitPat: hitPat}
|
||
}
|
||
|
||
// contains 按 spec 的 boundary 配置选择子串或词界匹配(I3)。
|
||
func (cs *compiledSpec) contains(hay, term string) bool {
|
||
if cs.spec.Matcher.Boundary {
|
||
return boundedContains(hay, term)
|
||
}
|
||
return strings.Contains(hay, term)
|
||
}
|
||
|
||
func compileSpec(spec IntentSpec, guard bool, index int, scoped map[string]SlotValidator, nfkc bool) (*compiledSpec, error) {
|
||
m := spec.Matcher
|
||
fold := func(terms []string) []string {
|
||
out := make([]string, len(terms))
|
||
for i, t := range terms {
|
||
out[i] = foldMatchText(t, nfkc, m.CaseSensitive)
|
||
}
|
||
return out
|
||
}
|
||
cs := &compiledSpec{
|
||
spec: spec,
|
||
termsAny: fold(m.IncludesAny),
|
||
termsAll: fold(m.IncludesAll),
|
||
termsEx: fold(m.ExcludesAny),
|
||
patterns: make([]*regexp.Regexp, len(m.Patterns)),
|
||
guard: guard,
|
||
index: index,
|
||
base: spec.Confidence.Base,
|
||
bonus: spec.Confidence.PatternBonus,
|
||
threshold: spec.Confidence.Threshold,
|
||
}
|
||
if cs.base == 0 {
|
||
cs.base = DefaultBase
|
||
}
|
||
if cs.bonus == 0 {
|
||
cs.bonus = DefaultPatternBonus
|
||
}
|
||
if cs.threshold == 0 {
|
||
cs.threshold = DefaultThreshold
|
||
}
|
||
for i, p := range m.Patterns {
|
||
re, err := regexp.Compile(p)
|
||
if err != nil {
|
||
return nil, newError(InvalidSpec, "pattern 编译失败", "field", fmt.Sprintf("matcher.patterns[%d]", i), "id", spec.ID, "error", err.Error())
|
||
}
|
||
cs.patterns[i] = re
|
||
}
|
||
for _, s := range spec.Slots {
|
||
cs2, err := compileSlot(s, scoped)
|
||
if err != nil {
|
||
return nil, err.(*Error).with("id", spec.ID)
|
||
}
|
||
cs.slots = append(cs.slots, cs2)
|
||
}
|
||
return cs, nil
|
||
}
|
||
|
||
// ---- 快照 ----
|
||
|
||
// Snapshot 是合并产物的不可变快照:全部 matcher 已编译,识别全程无锁读。
|
||
// 同一快照对同样输入给出同样结果;revision 单调递增。
|
||
type Snapshot struct {
|
||
rev int64
|
||
margin float64
|
||
topN int
|
||
maxInput int
|
||
specs []*compiledSpec // 仅启用 spec,按合并序
|
||
byID map[string]int
|
||
guardIdx []int
|
||
disabled []string // 已禁用 id(overlay disable 与 spec.disabled 两个入口)
|
||
sources map[string]string
|
||
total int // 含禁用
|
||
nfkc bool
|
||
}
|
||
|
||
// Revision 返回快照版本号,随每次成功 Reload 递增。
|
||
func (s *Snapshot) Revision() int64 { return s.rev }
|
||
|
||
// SpecIDs 返回启用中的 spec id,按合并序(确定性次序裁决的注册序)。
|
||
func (s *Snapshot) SpecIDs() []string {
|
||
ids := make([]string, len(s.specs))
|
||
for i, cs := range s.specs {
|
||
ids[i] = cs.spec.ID
|
||
}
|
||
return ids
|
||
}
|
||
|
||
// DisabledIDs 返回被禁用的 spec id,字典序。
|
||
func (s *Snapshot) DisabledIDs() []string {
|
||
out := make([]string, len(s.disabled))
|
||
copy(out, s.disabled)
|
||
sort.Strings(out)
|
||
return out
|
||
}
|
||
|
||
// Source 返回 spec 的来源描述("builtin" 或 "<file>#intents[i]")。
|
||
func (s *Snapshot) Source(id string) (string, bool) {
|
||
src, ok := s.sources[id]
|
||
return src, ok
|
||
}
|
||
|
||
// SpecByID 返回指定 id 的启用 spec 定义副本。切片与 Metadata 深拷贝,
|
||
// 调用方改返回值不会改到快照。不存在或已禁用时第二个返回值为 false。
|
||
func (s *Snapshot) SpecByID(id string) (IntentSpec, bool) {
|
||
if idx, ok := s.byID[id]; ok {
|
||
return cloneIntentSpec(s.specs[idx].spec), true
|
||
}
|
||
return IntentSpec{}, false
|
||
}
|
||
|
||
// Conflict 诊断信息:两个启用 spec 共享同一 includes_any 词条。
|
||
// 共享不一定是错误(计分与确定性次序会裁决),用于配置评审提示。
|
||
type Conflict struct {
|
||
Kind string
|
||
A string
|
||
B string
|
||
Term string
|
||
}
|
||
|
||
// Conflicts 报告非守卫 spec 之间共享的 includes_any 词条,确定性排序。
|
||
func (s *Snapshot) Conflicts() []Conflict {
|
||
termOwners := make(map[string][]string)
|
||
for _, cs := range s.specs {
|
||
if cs.guard {
|
||
continue
|
||
}
|
||
for _, t := range cs.spec.Matcher.IncludesAny {
|
||
termOwners[t] = append(termOwners[t], cs.spec.ID)
|
||
}
|
||
}
|
||
terms := make([]string, 0, len(termOwners))
|
||
for t, owners := range termOwners {
|
||
if len(owners) > 1 {
|
||
terms = append(terms, t)
|
||
}
|
||
}
|
||
sort.Strings(terms)
|
||
var out []Conflict
|
||
for _, t := range terms {
|
||
owners := append([]string(nil), termOwners[t]...)
|
||
sort.Strings(owners)
|
||
for i := 0; i < len(owners); i++ {
|
||
for j := i + 1; j < len(owners); j++ {
|
||
out = append(out, Conflict{Kind: "shared_term", A: owners[i], B: owners[j], Term: t})
|
||
}
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// specHit 规则阶段的内部命中记录。
|
||
type specHit struct {
|
||
cs *compiledSpec
|
||
score float64
|
||
terms []string
|
||
pats []string
|
||
}
|
||
|
||
func lessHit(a, b specHit) bool {
|
||
if a.score != b.score {
|
||
return a.score > b.score
|
||
}
|
||
if a.cs.spec.Priority != b.cs.spec.Priority {
|
||
return a.cs.spec.Priority > b.cs.spec.Priority
|
||
}
|
||
return a.cs.index < b.cs.index
|
||
}
|
||
|
||
func sortHits(hits []specHit) {
|
||
sort.SliceStable(hits, func(i, j int) bool { return lessHit(hits[i], hits[j]) })
|
||
}
|
||
|
||
// evalGuard 评估守卫域 spec,返回过阈命中(已排序)。
|
||
func (s *Snapshot) evalGuard(input, folded string) []specHit {
|
||
var hits []specHit
|
||
for _, idx := range s.guardIdx {
|
||
cs := s.specs[idx]
|
||
if o := cs.evaluate(input, folded); o.passed {
|
||
hits = append(hits, specHit{cs: cs, score: o.score, terms: o.hitTerms, pats: o.hitPat})
|
||
}
|
||
}
|
||
sortHits(hits)
|
||
return hits
|
||
}
|
||
|
||
// evalIDs 按给定 id 列表评估(挂起预期阶段用),返回过阈命中(已排序)。
|
||
func (s *Snapshot) evalIDs(ids []string, input, folded string) []specHit {
|
||
var hits []specHit
|
||
for _, id := range ids {
|
||
idx, ok := s.byID[id]
|
||
if !ok {
|
||
continue
|
||
}
|
||
cs := s.specs[idx]
|
||
if o := cs.evaluate(input, folded); o.passed {
|
||
hits = append(hits, specHit{cs: cs, score: o.score, terms: o.hitTerms, pats: o.hitPat})
|
||
}
|
||
}
|
||
sortHits(hits)
|
||
return hits
|
||
}
|
||
|
||
// evalRules 对全部非守卫 spec 做线性扫描,返回过阈命中(已排序)。
|
||
// 规模上限是 Options.MaxSpecs(默认 DefaultMaxSpecs = 2048)。倒排索引留到
|
||
// 超过该上限仍不够用时再做;构建期已经拒绝超过上限的快照。
|
||
func (s *Snapshot) evalRules(input, folded string) []specHit {
|
||
var hits []specHit
|
||
for _, cs := range s.specs {
|
||
if cs.guard {
|
||
continue
|
||
}
|
||
if o := cs.evaluate(input, folded); o.passed {
|
||
hits = append(hits, specHit{cs: cs, score: o.score, terms: o.hitTerms, pats: o.hitPat})
|
||
}
|
||
}
|
||
sortHits(hits)
|
||
return hits
|
||
}
|
||
|
||
func (h specHit) reason() string {
|
||
var parts []string
|
||
if len(h.terms) > 0 {
|
||
parts = append(parts, "terms="+strings.Join(h.terms, ","))
|
||
}
|
||
if len(h.pats) > 0 {
|
||
parts = append(parts, "patterns="+strings.Join(h.pats, ","))
|
||
}
|
||
return strings.Join(parts, " ")
|
||
}
|
||
|
||
func (h specHit) candidate(input string, withSlots bool) Candidate {
|
||
cs := h.cs
|
||
c := Candidate{
|
||
ID: cs.spec.ID,
|
||
Domain: cs.spec.Domain,
|
||
Version: cs.spec.Version,
|
||
Confidence: h.score,
|
||
Priority: cs.spec.Priority,
|
||
Risk: cs.spec.Risk,
|
||
RequiresConfirm: cs.spec.RequiresConfirm,
|
||
Handler: cs.spec.Handler,
|
||
Reason: h.reason(),
|
||
}
|
||
if c.Risk == "" {
|
||
c.Risk = RiskLow
|
||
}
|
||
if cs.spec.Action != nil {
|
||
ref := *cs.spec.Action
|
||
c.Action = &ref
|
||
}
|
||
if len(cs.spec.Metadata) > 0 {
|
||
c.Metadata = make(map[string]string, len(cs.spec.Metadata))
|
||
for k, v := range cs.spec.Metadata {
|
||
c.Metadata[k] = v
|
||
}
|
||
}
|
||
if withSlots {
|
||
c.Slots, c.MissingRequiredSlots, c.InvalidSlots = cs.extractSlots(input)
|
||
}
|
||
return c
|
||
}
|
||
|
||
// fillTopSlots 给裁决后的 Top 补抽槽位。Ambiguous 路径建候选时不抽槽位,
|
||
// 分类器选定 Top 之后才知道该抽哪一条。
|
||
func (s *Snapshot) fillTopSlots(c *Candidate, input string) {
|
||
idx, ok := s.byID[c.ID]
|
||
if !ok {
|
||
return
|
||
}
|
||
slots, missing, invalid := s.specs[idx].extractSlots(input)
|
||
c.Slots = slots
|
||
c.MissingRequiredSlots = missing
|
||
c.InvalidSlots = invalid
|
||
}
|
||
|
||
func candidatesOf(hits []specHit, input string, topSlots bool) []Candidate {
|
||
out := make([]Candidate, 0, len(hits))
|
||
for i, h := range hits {
|
||
out = append(out, h.candidate(input, topSlots && i == 0))
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ---- 构建与合并 ----
|
||
|
||
// RegistryBuilder 收集 Go 内置基线 spec 并构建 Registry。
|
||
type RegistryBuilder struct {
|
||
opts Options
|
||
builtin []IntentSpec
|
||
seen map[string]bool
|
||
}
|
||
|
||
// NewRegistryBuilder 创建构建器。opts 零值字段填默认。
|
||
func NewRegistryBuilder(opts Options) *RegistryBuilder {
|
||
return &RegistryBuilder{opts: opts.normalized(), seen: make(map[string]bool)}
|
||
}
|
||
|
||
// Register 登记内置基线 spec,立即校验;重复 id 报 InvalidSpec。
|
||
func (b *RegistryBuilder) Register(specs ...IntentSpec) error {
|
||
lim := b.opts.limits()
|
||
for _, spec := range specs {
|
||
if b.seen[spec.ID] {
|
||
return newError(InvalidSpec, "内置 spec id 重复", "id", spec.ID)
|
||
}
|
||
if err := validateSpec(spec, lim, b.opts.SlotValidators); err != nil {
|
||
return err
|
||
}
|
||
b.seen[spec.ID] = true
|
||
b.builtin = append(b.builtin, spec)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Build 仅用内置基线构建(rev 1)。此后 Reload 会报错(无配置来源)。
|
||
func (b *RegistryBuilder) Build() (*Registry, error) {
|
||
return b.buildWithSource(nil, nil)
|
||
}
|
||
|
||
// BuildFS 用内置基线 + JSON overlay 来源构建,并记住来源供 Reload 重读。
|
||
// roots 按给定顺序应用;目录 root 内按文件名字典序加载 *.json。
|
||
func (b *RegistryBuilder) BuildFS(ctx context.Context, fsys fs.FS, roots ...string) (*Registry, error) {
|
||
if fsys == nil {
|
||
return nil, newError(InvalidArgument, "fsys 为 nil")
|
||
}
|
||
if len(roots) == 0 {
|
||
return nil, newError(InvalidArgument, "至少提供一个配置 root")
|
||
}
|
||
return b.buildWithSource(fsys, roots)
|
||
}
|
||
|
||
func (b *RegistryBuilder) buildWithSource(fsys fs.FS, roots []string) (*Registry, error) {
|
||
var entries []overlayEntry
|
||
if fsys != nil {
|
||
var err error
|
||
entries, err = loadOverlays(fsys, roots)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
snap, err := buildSnapshot(b.opts, b.builtin, entries, 1)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
r := &Registry{
|
||
opts: b.opts,
|
||
builtin: append([]IntentSpec(nil), b.builtin...),
|
||
fsys: fsys,
|
||
roots: append([]string(nil), roots...),
|
||
hasSource: fsys != nil,
|
||
log: b.opts.Log,
|
||
}
|
||
r.snap.Store(snap)
|
||
return r, nil
|
||
}
|
||
|
||
// Registry 持有当前快照与配置来源,支持原子 Reload。
|
||
type Registry struct {
|
||
opts Options
|
||
builtin []IntentSpec
|
||
fsys fs.FS
|
||
roots []string
|
||
hasSource bool
|
||
mu sync.Mutex // 串行化 Reload
|
||
snap atomic.Pointer[Snapshot]
|
||
log *logsys.Logger
|
||
}
|
||
|
||
// Snapshot 返回当前生效快照。永不为 nil。
|
||
func (r *Registry) Snapshot() *Snapshot { return r.snap.Load() }
|
||
|
||
// Options 返回生效选项(已填默认值)。
|
||
func (r *Registry) Options() Options { return r.opts }
|
||
|
||
// Reload 重读配置来源并构建新快照;成功后原子替换并返回新快照。
|
||
// 任一步失败保留旧快照,revision 不变,返回带来源与原因的错误。
|
||
func (r *Registry) Reload(ctx context.Context) (*Snapshot, error) {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
old := r.snap.Load()
|
||
if !r.hasSource {
|
||
err := newError(LoadFailed, "没有登记配置来源;用 BuildFS 构建后才能 Reload", "revision", old.rev)
|
||
r.log.Module("intent").Error("intent config rejected", err)
|
||
return nil, err
|
||
}
|
||
entries, err := loadOverlays(r.fsys, r.roots)
|
||
if err != nil {
|
||
r.log.Module("intent").Error("intent config rejected", err)
|
||
return nil, err
|
||
}
|
||
ns, err := buildSnapshot(r.opts, r.builtin, entries, old.rev+1)
|
||
if err != nil {
|
||
r.log.Module("intent").Error("intent config rejected", err)
|
||
return nil, err
|
||
}
|
||
r.snap.Store(ns)
|
||
r.log.Module("intent").Info("intent snapshot reloaded",
|
||
"revision", ns.rev, "specs", len(ns.specs), "disabled", len(ns.disabled))
|
||
return ns, nil
|
||
}
|
||
|
||
// buildSnapshot 执行双源合并与编译。任一错误整批失败,不产生半套快照。
|
||
func buildSnapshot(opts Options, baseline []IntentSpec, entries []overlayEntry, rev int64) (*Snapshot, error) {
|
||
opts = opts.normalized()
|
||
protected := opts.protectedSet()
|
||
guards := opts.guardSet()
|
||
lim := opts.limits()
|
||
|
||
ordered := append([]IntentSpec(nil), baseline...)
|
||
pos := make(map[string]int, len(ordered))
|
||
sources := make(map[string]string, len(ordered))
|
||
for i, spec := range ordered {
|
||
pos[spec.ID] = i
|
||
sources[spec.ID] = "builtin"
|
||
}
|
||
var disabledList []string
|
||
|
||
for _, e := range entries {
|
||
if e.disable {
|
||
idx, ok := pos[e.id]
|
||
if !ok {
|
||
return nil, newError(InvalidSpec, "disable 目标不存在", "id", e.id, "source", e.source)
|
||
}
|
||
if protected[ordered[idx].Domain] {
|
||
return nil, newError(SpecProtected, "overlay 试图禁用保护域 spec",
|
||
"id", e.id, "domain", ordered[idx].Domain, "source", e.source)
|
||
}
|
||
ordered = append(ordered[:idx], ordered[idx+1:]...)
|
||
for id, p := range pos {
|
||
if p > idx {
|
||
pos[id] = p - 1
|
||
}
|
||
}
|
||
delete(pos, e.id)
|
||
disabledList = append(disabledList, e.id)
|
||
sources[e.id] = e.source + " (disabled)"
|
||
continue
|
||
}
|
||
spec := e.spec
|
||
if err := validateSpec(spec, lim, opts.SlotValidators); err != nil {
|
||
return nil, err.(*Error).with("source", e.source)
|
||
}
|
||
if idx, ok := pos[spec.ID]; ok {
|
||
old := ordered[idx]
|
||
if protected[old.Domain] {
|
||
return nil, newError(SpecProtected, "overlay 试图覆盖保护域 spec",
|
||
"id", spec.ID, "domain", old.Domain, "source", e.source)
|
||
}
|
||
if spec.Version <= old.Version {
|
||
return nil, newError(InvalidSpec, "覆盖必须递增 version",
|
||
"id", spec.ID, "old_version", old.Version, "new_version", spec.Version, "source", e.source)
|
||
}
|
||
ordered[idx] = spec
|
||
} else {
|
||
ordered = append(ordered, spec)
|
||
pos[spec.ID] = len(ordered) - 1
|
||
}
|
||
sources[spec.ID] = e.source
|
||
}
|
||
|
||
total := len(ordered) + len(disabledList)
|
||
if total > lim.maxSpecs {
|
||
return nil, newError(InvalidSpec, "spec 总数超过上限", "total", total, "max", lim.maxSpecs)
|
||
}
|
||
|
||
snap := &Snapshot{
|
||
rev: rev,
|
||
margin: opts.AmbiguityMargin,
|
||
topN: opts.TopN,
|
||
maxInput: opts.MaxInputLen,
|
||
byID: make(map[string]int, len(ordered)),
|
||
disabled: disabledList,
|
||
sources: sources,
|
||
total: total,
|
||
nfkc: opts.NFKC,
|
||
}
|
||
nextIndex := 0
|
||
for _, spec := range ordered {
|
||
if spec.Disabled {
|
||
snap.disabled = append(snap.disabled, spec.ID)
|
||
continue
|
||
}
|
||
cs, err := compileSpec(spec, guards[spec.Domain], nextIndex, opts.SlotValidators, opts.NFKC)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
snap.specs = append(snap.specs, cs)
|
||
snap.byID[spec.ID] = len(snap.specs) - 1
|
||
if cs.guard {
|
||
snap.guardIdx = append(snap.guardIdx, len(snap.specs)-1)
|
||
}
|
||
nextIndex++
|
||
}
|
||
return snap, nil
|
||
}
|