Files
jbergner 4902388ef1
release-tag / release-image (push) Successful in 11m2s
1.5.8
2026-08-27 14:06:37 +02:00

174 lines
5.7 KiB
Go

package brain
import (
"fmt"
"regexp"
"strconv"
"strings"
"neuroforge/internal/core"
"neuroforge/internal/store"
"neuroforge/internal/vector"
)
var targetNumberRE = regexp.MustCompile(`(?i)(\d{1,9})`)
func (e *Engine) refreshGoalResearchProgress(goal *core.Goal, evaluation float64) {
if goal == nil {
return
}
// Recompute from relevant evidence instead of keeping monotonic counters from
// old research runs. This intentionally lets upgrades remove previously
// counted off-topic evidence (for example an NVIDIA goal polluted by WebRTC).
sourceSet := map[string]struct{}{}
memorySet := map[string]struct{}{}
corroborationSet := map[string]struct{}{}
runs := e.store.ResearchRunsSnapshot(goal.ID, 200)
for _, run := range runs {
for _, ev := range run.Events {
if ev.Type != "evidence.learned" && ev.Type != "evidence.corroborated" {
continue
}
m, ok := e.store.GetMemory(ev.MemoryID)
if !ok || m == nil {
continue
}
var src *core.KnowledgeSource
if m.Provenance.SourceID != "" {
if x, ok := e.store.GetSource(m.Provenance.SourceID); ok {
src = x
}
}
if !goalEvidenceRelevant(goal, *m, src) {
continue
}
memorySet[m.ID] = struct{}{}
if ev.SourceID != "" {
sourceSet[ev.SourceID] = struct{}{}
} else if m.Provenance.SourceID != "" {
sourceSet[m.Provenance.SourceID] = struct{}{}
}
if ev.Type == "evidence.corroborated" && ev.SourceID != "" {
corroborating, ok := e.store.GetSource(ev.SourceID)
if ok && corroborating != nil {
primaryOrigin := ""
if src != nil {
primaryOrigin = sourceOriginKey(src.URI)
}
origin := sourceOriginKey(corroborating.URI)
if origin != "" && origin != primaryOrigin {
corroborationSet[m.ID+"\x00"+origin] = struct{}{}
}
}
}
}
}
// Durable provenance/legacy goal tags cover evidence older than the bounded
// research-run history and make the relevance repair effective after restart.
for _, m := range e.store.MemoriesSnapshot() {
if !memoryBelongsToGoal(m, goal.ID) || m.Provenance.SourceID == "" {
continue
}
var src *core.KnowledgeSource
if x, ok := e.store.GetSource(m.Provenance.SourceID); ok {
src = x
}
if !goalEvidenceRelevant(goal, m, src) {
continue
}
memorySet[m.ID] = struct{}{}
sourceSet[m.Provenance.SourceID] = struct{}{}
primaryOrigin := ""
if src != nil {
primaryOrigin = sourceOriginKey(src.URI)
}
for _, sid := range m.EvidenceSourceIDs {
if sid == "" || sid == m.Provenance.SourceID {
continue
}
corroborating, ok := e.store.GetSource(sid)
if !ok || corroborating == nil {
continue
}
origin := sourceOriginKey(corroborating.URI)
if origin != "" && origin != primaryOrigin {
corroborationSet[m.ID+"\x00"+origin] = struct{}{}
}
}
}
goal.ResearchEvidence = len(memorySet)
goal.ResearchSources = len(sourceSet)
goal.ResearchCorroborations = len(corroborationSet)
goal.ResearchSourceIDs = goal.ResearchSourceIDs[:0]
for id := range sourceSet {
goal.ResearchSourceIDs = append(goal.ResearchSourceIDs, id)
if len(goal.ResearchSourceIDs) >= 512 {
break
}
}
target := strings.ToLower(strings.TrimSpace(goal.Target))
if m := targetNumberRE.FindStringSubmatch(target); len(m) == 2 {
if n, err := strconv.Atoi(m[1]); err == nil && n > 0 {
current, label := goal.ResearchEvidence, "quellengebundene Evidenzen"
if strings.Contains(target, "artikel") || strings.Contains(target, "article") || strings.Contains(target, "draft") || strings.Contains(target, "entwurf") {
current, label = 0, "validierte Staging-Artikel"
if goal.LastStagingDraftID != "" && goal.StagingDraftValidated && goal.StagingQualityGateVersion == stagingQualityGateVersion {
current = 1
}
} else {
evidenceTarget := strings.Contains(target, "wissensein") || strings.Contains(target, "evidenz") || strings.Contains(target, "claim") || strings.Contains(target, "eintr")
if !evidenceTarget && (strings.Contains(target, "quelle") || strings.Contains(target, "source")) {
current, label = goal.ResearchSources, "unabhängige Quellen"
}
if strings.Contains(target, "bestät") || strings.Contains(target, "corrobor") {
current, label = goal.ResearchCorroborations, "Bestätigungen"
}
}
goal.Progress = vector.Clamp(float64(current)/float64(n), 0, 1)
goal.ProgressReason = fmt.Sprintf("%d/%d %s", current, n, label)
return
}
}
sat := func(v, target int) float64 {
if target <= 0 {
return 0
}
return vector.Clamp(float64(v)/float64(target), 0, 1)
}
quality := vector.Clamp((evaluation+1)/2, 0, 1)
goal.Progress = vector.Clamp(.55*sat(goal.ResearchEvidence, 20)+.25*sat(goal.ResearchSources, 8)+.15*sat(goal.ResearchCorroborations, 5)+.05*quality, 0, 1)
goal.ProgressReason = fmt.Sprintf("%d Evidenzen · %d Quellen · %d Bestätigungen", goal.ResearchEvidence, goal.ResearchSources, goal.ResearchCorroborations)
}
func filterGoalEvidenceHits(hits []store.SearchHit) []store.SearchHit {
out := make([]store.SearchHit, 0, len(hits))
for _, h := range hits {
if h.Memory.Kind == "goal-learning" || h.Memory.Provenance.Source == "goal-cycle" {
continue
}
out = append(out, h)
}
return out
}
// ReconcileGoalProgress backfills the measurable progress fields from persisted
// research-run telemetry. It makes upgrades immediately reflect historical work
// without requiring a fresh web-research cycle first.
func (e *Engine) ReconcileGoalProgress() error {
for _, g := range e.store.GoalsSnapshot() {
if !g.ResearchEnabled {
continue
}
before, beforeReason := g.Progress, g.ProgressReason
e.refreshGoalResearchProgress(&g, g.LastEvaluation)
if g.Progress != before || g.ProgressReason != beforeReason {
if err := e.store.UpsertGoal(&g); err != nil {
return err
}
}
}
return nil
}