156 lines
5.7 KiB
Go
156 lines
5.7 KiB
Go
package pipeline
|
||
|
||
import (
|
||
"sort"
|
||
|
||
"textmachine/backend/internal/lang"
|
||
)
|
||
|
||
// miner.go: the WS3 bank-mining detector orchestration (a Go port of the frozen exp16 arm A3 = V-C,
|
||
// LEMMATIZER-DEFAULT-B). Default B drops the pymorphy3-backed sub-channels the ratified §10-14 default
|
||
// removes: the spread multiplier (λ=0 already zeroed it), the dst-variants co-occurrence, and the
|
||
// Palladius ru-side name confirmation (+56 bonus). The dst (WHAT) is delivered by the banknote (WS4),
|
||
// never by co-occurrence, so exact rank is not a product value — the invariant guarantees are the
|
||
// candidate SET (membership), recall@proposed, and the catastrophe screen (∈top-50); dropping the
|
||
// Palladius sub-channel moves ONLY 古月's exact rank (21→22 vs the pymorphy3 reference), screen still
|
||
// PASS. Pure and deterministic ($0, no LLM, no network, no time/rand).
|
||
|
||
// MinerConfig is the frozen miner-v1 config (thresholds tuned on chapters 1–15, §D1). Values mirror
|
||
// arms.FROZEN; the pymorphy3-dependent knobs (lam, spread_freq_min) are absent by default-B design.
|
||
type MinerConfig struct {
|
||
FreqFloor int
|
||
SubsumeAlpha float64
|
||
FormantMinPartners int
|
||
FormantMinOverRep float64
|
||
Bonus map[string]float64 // typ → base bonus (name/place/title/term)
|
||
PatternWeight map[string]float64 // evidence-source → weight (max over a candidate's evidence)
|
||
SubfloorFreqScale float64
|
||
TopK int
|
||
PackVersion string
|
||
}
|
||
|
||
// frozenMinerConfig is the ratified default-B config (versionable — a threshold edit is a mining-config
|
||
// bump the caller folds; a ±50% sweep is invariant, A3@f≥3=1.0 holds by floor/over_rep/subsume/partners).
|
||
func frozenMinerConfig() MinerConfig {
|
||
return MinerConfig{
|
||
FreqFloor: minerFreqFloor,
|
||
SubsumeAlpha: minerSubsumeAlpha,
|
||
FormantMinPartners: 3,
|
||
FormantMinOverRep: 15.0,
|
||
Bonus: map[string]float64{"name": 140.0, "place": 140.0, "title": 120.0, "term": 80.0},
|
||
PatternWeight: map[string]float64{
|
||
"surname": 1.0, "ordinal_title": 1.0, "title_suffix": 0.9, "topo_suffix": 0.9,
|
||
"rank_grade": 1.0, "formant_suffix": 0.6, "formant_prefix": 0.5, "title_bare": 0.4,
|
||
"palladius": 0.8, // retained for parity of the weight table; the sub-channel is off in default B
|
||
},
|
||
SubfloorFreqScale: 40.0,
|
||
TopK: 90,
|
||
PackVersion: minerPackVersion,
|
||
}
|
||
}
|
||
|
||
// ScoredCand is one candidate after the V-C arm assembly (arms.ScoredCand, minus the default-B-dropped
|
||
// dst_variants/spread). types/evidence are in deterministic first-seen order; fromPattern marks a
|
||
// pattern-channel contribution.
|
||
type ScoredCand struct {
|
||
Src string
|
||
Score float64
|
||
Freq int
|
||
Types []string
|
||
Evidence []string
|
||
FromPattern bool
|
||
}
|
||
|
||
// mineResult carries the detector outputs the alias/emit layers consume.
|
||
type mineResult struct {
|
||
ranked []ScoredCand // A3 ranking, sorted (-score,-freq,src)
|
||
subsumed map[string]bool // V-A ranking-dropped boundary fragments
|
||
formants map[rune]formantInfo
|
||
cfg MinerConfig
|
||
}
|
||
|
||
// mineDetect runs V-A → V-C (arm A3, default B) over the normalized chunks and returns the ranked
|
||
// candidate list. contrast is the general-zh reference (loaded once). Deterministic.
|
||
func mineDetect(chunks []MinerChunk, contrast *Contrast, cfg MinerConfig, pack *lang.Pack) mineResult {
|
||
va := runVA(chunks, contrast, cfg.FreqFloor, cfg.SubsumeAlpha)
|
||
pats, formants := patternCandidates(chunks, va.candFreq, contrast, cfg.FormantMinPartners, cfg.FormantMinOverRep, pack)
|
||
|
||
merged := make(map[string]*ScoredCand, len(va.ranked)+len(pats))
|
||
for _, c := range va.ranked {
|
||
merged[c.src] = &ScoredCand{Src: c.src, Score: c.score, Freq: c.freq} // score·(1+λ·sp) with λ=0
|
||
}
|
||
for cand, info := range pats {
|
||
pw := 0.3
|
||
for _, ev := range info.evidence {
|
||
if w, ok := cfg.PatternWeight[evSource(ev)]; ok && w > pw {
|
||
pw = w
|
||
}
|
||
}
|
||
typ := "term"
|
||
if len(info.types) > 0 {
|
||
typ = info.types[0]
|
||
}
|
||
base, ok := cfg.Bonus[typ]
|
||
if !ok {
|
||
base = cfg.Bonus["term"]
|
||
}
|
||
bonus := base * pw
|
||
if sc, ok := merged[cand]; ok {
|
||
sc.Score += bonus
|
||
for _, t := range info.types {
|
||
if !containsStr(sc.Types, t) {
|
||
sc.Types = append(sc.Types, t)
|
||
}
|
||
}
|
||
sc.Evidence = append(sc.Evidence, firstN(info.evidence, 3)...)
|
||
sc.FromPattern = true
|
||
} else {
|
||
f := countOccurrences(cand, chunks)
|
||
merged[cand] = &ScoredCand{
|
||
Src: cand, Score: bonus + cfg.SubfloorFreqScale*float64(f)*pw, Freq: f,
|
||
Types: append([]string(nil), info.types...), Evidence: firstN(info.evidence, 3),
|
||
FromPattern: true,
|
||
}
|
||
}
|
||
}
|
||
// Palladius ru-side confirmation loop is INTENTIONALLY OMITTED (default B — it needs dst_variants +
|
||
// is_name_lemma, both pymorphy3; it only shifts 古月's exact rank, which is not a product value).
|
||
|
||
ranked := make([]ScoredCand, 0, len(merged))
|
||
for _, sc := range merged {
|
||
ranked = append(ranked, *sc)
|
||
}
|
||
sortScored(ranked)
|
||
return mineResult{ranked: ranked, subsumed: va.subsumed, formants: formants, cfg: cfg}
|
||
}
|
||
|
||
// sortScored orders the arm ranking by (-score, -freq, src) — the frozen key.
|
||
func sortScored(cs []ScoredCand) {
|
||
sort.Slice(cs, func(i, j int) bool {
|
||
if cs[i].Score != cs[j].Score {
|
||
return cs[i].Score > cs[j].Score
|
||
}
|
||
if cs[i].Freq != cs[j].Freq {
|
||
return cs[i].Freq > cs[j].Freq
|
||
}
|
||
return cs[i].Src < cs[j].Src
|
||
})
|
||
}
|
||
|
||
// evSource is the pattern-weight key: the evidence tag before its ':' (arms' ev.split(":")[0]).
|
||
func evSource(ev string) string {
|
||
for i := 0; i < len(ev); i++ {
|
||
if ev[i] == ':' {
|
||
return ev[:i]
|
||
}
|
||
}
|
||
return ev
|
||
}
|
||
|
||
// firstN returns up to n elements of ss (a fresh slice, never aliasing the input).
|
||
func firstN(ss []string, n int) []string {
|
||
if len(ss) < n {
|
||
n = len(ss)
|
||
}
|
||
return append([]string(nil), ss[:n]...)
|
||
}
|