1.5.8
release-tag / release-image (push) Successful in 11m2s

This commit is contained in:
2026-08-27 14:06:37 +02:00
parent 5899989228
commit 4902388ef1
14 changed files with 810 additions and 63 deletions
@@ -112,7 +112,10 @@ func (e *Engine) refreshGoalResearchProgress(goal *core.Goal, evaluation float64
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 = goal.StagingDraftsCreated, "Staging-Artikel"
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")) {
@@ -286,17 +286,17 @@ func TestResearchMaterialRelevanceRejectsOffTopicWebRTCForNVIDIA(t *testing.T) {
}
}
func TestGoalArticleTargetUsesCreatedStagingArticles(t *testing.T) {
func TestGoalArticleTargetUsesCurrentValidatedStagingDraft(t *testing.T) {
s, err := store.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer s.Close()
g := &core.Goal{ID: "goal-articles", Title: "NVIDIA", Target: "20 hochwertige Wissensartikel", StagingDraftsCreated: 1}
g := &core.Goal{ID: "goal-articles", Title: "NVIDIA", Target: "20 hochwertige Wissensartikel", StagingDraftsCreated: 1, LastStagingDraftID: "KB-1", StagingDraftValidated: true, StagingQualityGateVersion: stagingQualityGateVersion}
e := &Engine{store: s}
e.refreshGoalResearchProgress(g, 0)
if g.Progress < .049 || g.Progress > .051 || !strings.Contains(g.ProgressReason, "1/20 Staging-Artikel") {
t.Fatalf("article target must count articles, got progress=%f reason=%q", g.Progress, g.ProgressReason)
if g.Progress < .049 || g.Progress > .051 || !strings.Contains(g.ProgressReason, "1/20 validierte Staging-Artikel") {
t.Fatalf("article target must count only a current validated draft, got progress=%f reason=%q", g.Progress, g.ProgressReason)
}
}
@@ -630,3 +630,92 @@ func TestAuthoritativeDomainConfigurationRejectsOverbroadValues(t *testing.T) {
t.Fatalf("unsafe authority domains were not sanitized: %#v", cfg.AuthoritativeDomains)
}
}
func TestStagingRetriesOldQualityFailureOnceAfterGateUpgrade(t *testing.T) {
requests := 0
kb := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"staging": map[string]any{"key": "KB-AI-STAGING-OLD", "meta": map[string]any{"integration_action": "updated"}}})
}))
defer kb.Close()
s, err := store.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer s.Close()
src := &core.KnowledgeSource{ID: "src-1", Type: "web", Title: "Fortinet 7200", URI: "https://community.fortinet.com/fortigate/7200", Trust: .9, Status: "ready"}
if err := s.UpsertSource(src); err != nil {
t.Fatal(err)
}
mem := &core.Memory{ID: "mem-1", Kind: "evidence", MemoryType: core.MemorySemantic, Text: "FortiClient SSLVPN error 7200 troubleshooting evidence.", Confidence: .8, Status: core.MemoryActive, Provenance: core.MemoryProvenance{Source: "web.page", GoalID: "goal-1", SourceID: src.ID}}
if err := s.AddMemory(mem); err != nil {
t.Fatal(err)
}
run, err := s.StartResearchRun("goal-1", "FortiClient SSLVPN 7200")
if err != nil {
t.Fatal(err)
}
_, _ = s.FinishResearchRun(run.ID, "completed", "")
e := &Engine{store: s, http: kb.Client()}
e.ConfigureStagingPublisher(StagingPublisherConfig{Enabled: true, URL: kb.URL, Token: "secret", MinEvidence: 1, MinSources: 1, SynthesisMode: "evidence"})
g := &core.Goal{
ID: "goal-1", Title: "FortiClient SSLVPN 7200", ResearchEvidence: 1, ResearchSources: 1,
LastStagingDraftID: "KB-AI-STAGING-OLD",
LastStagingError: "staging synthesis introduced source-unverified identifiers: /portal-konfiguration",
}
e.maybePublishGoalDraft(context.Background(), g, ResearchResult{RunID: run.ID})
if requests != 1 || g.LastStagingError != "" || !g.StagingDraftValidated || g.StagingQualityGateVersion != stagingQualityGateVersion {
t.Fatalf("old quality failure was not revalidated: requests=%d goal=%#v", requests, g)
}
// The same unchanged, already validated evidence must not be synthesized again.
e.maybePublishGoalDraft(context.Background(), g, ResearchResult{RunID: run.ID})
if requests != 1 {
t.Fatalf("validated unchanged draft was republished: requests=%d", requests)
}
}
func TestArticleProgressRequiresCurrentValidatedDraft(t *testing.T) {
s, err := store.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer s.Close()
e := &Engine{store: s}
g := &core.Goal{ID: "g1", Title: "Test", Target: "1 hochwertiger Wissensartikel", LastStagingDraftID: "KB-OLD", StagingDraftsCreated: 1}
e.refreshGoalResearchProgress(g, 0)
if g.Progress != 0 || !strings.Contains(g.ProgressReason, "0/1 validierte Staging-Artikel") {
t.Fatalf("unvalidated legacy draft must not satisfy article target: %#v", g)
}
g.StagingDraftValidated = true
g.StagingQualityGateVersion = stagingQualityGateVersion
e.refreshGoalResearchProgress(g, 0)
if g.Progress != 1 || !strings.Contains(g.ProgressReason, "1/1 validierte Staging-Artikel") {
t.Fatalf("current validated draft should satisfy target: %#v", g)
}
}
func TestStagingBelowThresholdInvalidatesLegacyDraftAndReplacesStaleError(t *testing.T) {
s, err := store.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer s.Close()
e := &Engine{store: s}
e.ConfigureStagingPublisher(StagingPublisherConfig{Enabled: true, URL: "http://knowledge.invalid", Token: "secret", MinEvidence: 4, MinSources: 2})
g := &core.Goal{
ID: "g1", LastStagingDraftID: "KB-OLD", StagingDraftValidated: true,
StagingQualityGateVersion: "staging-v2",
LastStagingError: "staging synthesis introduced source-unverified identifiers: /portal-konfiguration",
}
e.maybePublishGoalDraft(context.Background(), g, ResearchResult{})
if g.StagingDraftValidated || g.StagingQualityGateVersion != stagingQualityGateVersion || !strings.Contains(g.LastStagingError, "quality gate not satisfied") {
t.Fatalf("legacy draft state was not invalidated/reconciled: %#v", g)
}
}
+80 -13
View File
@@ -3,6 +3,7 @@ package brain
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
@@ -14,6 +15,8 @@ import (
"neuroforge/internal/core"
)
const stagingQualityGateVersion = "staging-v3"
// StagingPublisherConfig configures the one-way governance bridge from
// autonomous research into the human-review knowledge staging area.
type StagingPublisherConfig struct {
@@ -120,25 +123,25 @@ func (e *Engine) maybePublishGoalDraft(ctx context.Context, goal *core.Goal, res
return
}
if strings.TrimSpace(cfg.URL) == "" || strings.TrimSpace(cfg.Token) == "" {
goal.StagingDraftValidated = false
goal.StagingQualityGateVersion = stagingQualityGateVersion
goal.LastStagingError = "staging publisher enabled but URL/token is missing"
_ = e.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "staging.publish_error", Summary: "Research draft could not be published", Reason: goal.LastStagingError, Actor: "goal-learning", Metadata: map[string]string{"goal_id": goal.ID}})
return
}
if goal.LastStagingDraftID != "" && research.RunID != "" {
if run, ok := e.store.GetResearchRun(research.RunID); ok && run.Stats.NewEvidence == 0 && run.Stats.Corroborations == 0 {
// Avoid rewriting the same active staging draft every scheduler tick when
// this cycle contributed no new information.
return
}
}
if goal.ResearchEvidence < cfg.MinEvidence || goal.ResearchSources < cfg.MinSources || goal.ResearchCorroborations < cfg.MinCorroborations {
goal.LastStagingError = ""
_ = e.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "staging.not_ready", Summary: "Research has not reached staging quality gate", Reason: fmt.Sprintf("evidence=%d/%d sources=%d/%d corroborations=%d/%d", goal.ResearchEvidence, cfg.MinEvidence, goal.ResearchSources, cfg.MinSources, goal.ResearchCorroborations, cfg.MinCorroborations), Actor: "goal-learning", Metadata: map[string]string{"goal_id": goal.ID}})
goal.StagingDraftValidated = false
goal.StagingQualityGateVersion = stagingQualityGateVersion
goal.LastStagingAttemptSignature = ""
goal.LastStagingError = fmt.Sprintf("staging quality gate not satisfied: evidence=%d/%d sources=%d/%d corroborations=%d/%d", goal.ResearchEvidence, cfg.MinEvidence, goal.ResearchSources, cfg.MinSources, goal.ResearchCorroborations, cfg.MinCorroborations)
_ = e.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "staging.not_ready", Summary: "Research has not reached staging quality gate", Reason: goal.LastStagingError, Actor: "goal-learning", Metadata: map[string]string{"goal_id": goal.ID}})
return
}
evidence := e.collectGoalDraftEvidence(goal, cfg)
if len(evidence) == 0 {
goal.StagingDraftValidated = false
goal.StagingQualityGateVersion = stagingQualityGateVersion
goal.LastStagingError = "no active, goal-relevant source-backed evidence available for staging"
return
}
@@ -158,15 +161,39 @@ func (e *Engine) maybePublishGoalDraft(ctx context.Context, goal *core.Goal, res
}
}
if len(selectedSources) < cfg.MinSources {
goal.StagingDraftValidated = false
goal.StagingQualityGateVersion = stagingQualityGateVersion
goal.LastStagingError = fmt.Sprintf("staging evidence diversity below threshold: relevant_sources=%d/%d", len(selectedSources), cfg.MinSources)
return
}
authoritativeSources, independentOrigins, sourceAudit := summarizeEvidenceAuthority(cfg, evidence)
if cfg.RequireAuthoritativeSource && authoritativeSources < cfg.MinAuthoritativeSources {
goal.StagingDraftValidated = false
goal.StagingQualityGateVersion = stagingQualityGateVersion
goal.LastStagingError = fmt.Sprintf("staging source authority below threshold: authoritative_sources=%d/%d", authoritativeSources, cfg.MinAuthoritativeSources)
_ = e.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "staging.not_ready", Summary: "Research draft lacks authoritative sources", Reason: goal.LastStagingError, Actor: "goal-learning", Metadata: map[string]string{"goal_id": goal.ID}})
return
}
attemptSignature := stagingEvidenceSignature(evidence)
noNewInformation := false
if research.RunID != "" {
if run, ok := e.store.GetResearchRun(research.RunID); ok {
noNewInformation = run.Stats.NewEvidence == 0 && run.Stats.Corroborations == 0
}
}
if goal.LastStagingDraftID != "" && noNewInformation {
if goal.StagingDraftValidated && goal.StagingQualityGateVersion == stagingQualityGateVersion && strings.TrimSpace(goal.LastStagingError) == "" {
return
}
if goal.StagingQualityGateVersion == stagingQualityGateVersion && goal.LastStagingAttemptSignature == attemptSignature && deterministicStagingFailure(goal.LastStagingError) {
return
}
}
goal.StagingDraftValidated = false
goal.StagingQualityGateVersion = stagingQualityGateVersion
goal.LastStagingAttemptSignature = attemptSignature
draft, err := e.synthesizeGoalDraft(ctx, goal, evidence)
if err != nil {
goal.LastStagingError = err.Error()
@@ -205,7 +232,7 @@ func (e *Engine) maybePublishGoalDraft(ctx context.Context, goal *core.Goal, res
"research_evidence_ids": evidenceIDs,
"research_source_uris": sourceURIs,
"source_authority": sourceAudit,
"quality_gate_version": "staging-v2",
"quality_gate_version": stagingQualityGateVersion,
"human_review_required": true,
}
if draft.Quality != nil && draft.Quality.Verification != nil {
@@ -245,9 +272,49 @@ func (e *Engine) maybePublishGoalDraft(ctx context.Context, goal *core.Goal, res
}
goal.LastStagingDraftID = out.Staging.Key
goal.LastStagingError = ""
goal.StagingDraftValidated = true
goal.StagingQualityGateVersion = stagingQualityGateVersion
goal.LastStagingAttemptSignature = attemptSignature
_ = e.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "staging.draft_" + firstNonEmpty(action, "created"), Summary: "Research proposal sent to human-review staging", Reason: "research quality gate satisfied", Actor: "goal-learning", Metadata: map[string]string{"goal_id": goal.ID, "staging_id": out.Staging.Key, "action": action}})
}
func stagingEvidenceSignature(evidence []draftEvidence) string {
parts := make([]string, 0, len(evidence)*2+1)
parts = append(parts, stagingQualityGateVersion)
for _, ev := range evidence {
parts = append(parts, ev.Memory.ID, ev.Memory.Provenance.SourceID)
for _, src := range ev.CorroboratingSources {
if src != nil {
parts = append(parts, src.ID)
}
}
}
sort.Strings(parts[1:])
sum := sha256.Sum256([]byte(strings.Join(parts, "\x00")))
return fmt.Sprintf("%s:%x", stagingQualityGateVersion, sum[:12])
}
func deterministicStagingFailure(message string) bool {
m := strings.ToLower(strings.TrimSpace(message))
if m == "" {
return false
}
for _, prefix := range []string{
"staging synthesis ",
"invalid staging synthesis ",
"claim verification ",
"staging source authority ",
"staging evidence diversity ",
"no active, goal-relevant ",
"staging quality gate ",
} {
if strings.HasPrefix(m, prefix) {
return true
}
}
return strings.Contains(m, "source-unverified identifiers")
}
func (e *Engine) collectGoalDraftEvidence(goal *core.Goal, cfg StagingPublisherConfig) []draftEvidence {
if goal == nil {
return nil
@@ -412,7 +479,7 @@ func (e *Engine) synthesizeGoalDraft(ctx context.Context, goal *core.Goal, evide
Text: "Automatisch recherchiertes Evidence-Bundle. Keine Artikelsynthese; menschliche Prüfung ist zwingend erforderlich.\n\n" + evidencePack,
Answer: answer, Categories: []string{"Research", goal.Title}, Keywords: goalKeywords(goal), MinScore: .85,
IntegrationKey: "neuroforge-goal:" + goal.ID,
Quality: &stagingQualityMetadata{GateVersion: "staging-v2", AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit},
Quality: &stagingQualityMetadata{GateVersion: stagingQualityGateVersion, AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit},
}, nil
}
if cfg.SynthesisMode != "llm" {
@@ -473,7 +540,7 @@ func (e *Engine) synthesizeGoalDraft(ctx context.Context, goal *core.Goal, evide
return stagingDraftPayload{}, err
}
auth, origins, audit := summarizeEvidenceAuthority(cfg, evidence)
draft.Quality = &stagingQualityMetadata{GateVersion: "staging-v2", AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit}
draft.Quality = &stagingQualityMetadata{GateVersion: stagingQualityGateVersion, AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit}
if !cfg.VerifyClaims {
return draft, nil
}
@@ -485,7 +552,7 @@ func (e *Engine) synthesizeGoalDraft(ctx context.Context, goal *core.Goal, evide
repairedReport, secondErr := e.verifyDraftClaims(ctx, goal, evidence, repairedDraft)
if secondErr == nil {
repairedReport.RepairApplied = true
repairedDraft.Quality = &stagingQualityMetadata{GateVersion: "staging-v2", AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit, Verification: &repairedReport}
repairedDraft.Quality = &stagingQualityMetadata{GateVersion: stagingQualityGateVersion, AuthoritativeSources: auth, IndependentOrigins: origins, SourceAudit: audit, Verification: &repairedReport}
return repairedDraft, nil
}
verifyErr = fmt.Errorf("%v; grounded repair verification failed: %w", verifyErr, secondErr)
+33 -30
View File
@@ -523,36 +523,39 @@ type MaintenanceStatus struct {
}
type Goal struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Status string `json:"status"`
Priority int `json:"priority"`
Progress float64 `json:"progress"`
Target string `json:"target,omitempty"`
Prediction string `json:"prediction,omitempty"`
NextAction string `json:"next_action,omitempty"`
LastEvaluation float64 `json:"last_evaluation,omitempty"`
ProgressReason string `json:"progress_reason,omitempty"`
ResearchEvidence int `json:"research_evidence,omitempty"`
ResearchSources int `json:"research_sources,omitempty"`
ResearchCorroborations int `json:"research_corroborations,omitempty"`
ResearchSourceIDs []string `json:"research_source_ids,omitempty"`
StagingDraftsCreated int `json:"staging_drafts_created,omitempty"`
StagingDraftsUpdated int `json:"staging_drafts_updated,omitempty"`
LastStagingDraftID string `json:"last_staging_draft_id,omitempty"`
LastStagingError string `json:"last_staging_error,omitempty"`
MemoryIDs []string `json:"memory_ids,omitempty"`
Tags []string `json:"tags,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastCycleAt time.Time `json:"last_cycle_at,omitempty"`
AutoRun bool `json:"auto_run"`
IntervalMinutes int `json:"interval_minutes,omitempty"`
NextCycleAt time.Time `json:"next_cycle_at,omitempty"`
ResearchEnabled bool `json:"research_enabled"`
ConsecutiveErrors int `json:"consecutive_errors,omitempty"`
LastError string `json:"last_error,omitempty"`
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Status string `json:"status"`
Priority int `json:"priority"`
Progress float64 `json:"progress"`
Target string `json:"target,omitempty"`
Prediction string `json:"prediction,omitempty"`
NextAction string `json:"next_action,omitempty"`
LastEvaluation float64 `json:"last_evaluation,omitempty"`
ProgressReason string `json:"progress_reason,omitempty"`
ResearchEvidence int `json:"research_evidence,omitempty"`
ResearchSources int `json:"research_sources,omitempty"`
ResearchCorroborations int `json:"research_corroborations,omitempty"`
ResearchSourceIDs []string `json:"research_source_ids,omitempty"`
StagingDraftsCreated int `json:"staging_drafts_created,omitempty"`
StagingDraftsUpdated int `json:"staging_drafts_updated,omitempty"`
LastStagingDraftID string `json:"last_staging_draft_id,omitempty"`
LastStagingError string `json:"last_staging_error,omitempty"`
StagingDraftValidated bool `json:"staging_draft_validated,omitempty"`
StagingQualityGateVersion string `json:"staging_quality_gate_version,omitempty"`
LastStagingAttemptSignature string `json:"last_staging_attempt_signature,omitempty"`
MemoryIDs []string `json:"memory_ids,omitempty"`
Tags []string `json:"tags,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastCycleAt time.Time `json:"last_cycle_at,omitempty"`
AutoRun bool `json:"auto_run"`
IntervalMinutes int `json:"interval_minutes,omitempty"`
NextCycleAt time.Time `json:"next_cycle_at,omitempty"`
ResearchEnabled bool `json:"research_enabled"`
ConsecutiveErrors int `json:"consecutive_errors,omitempty"`
LastError string `json:"last_error,omitempty"`
}
type LearningCycle struct {
+6
View File
@@ -456,6 +456,12 @@ components:
type: string
last_staging_error:
type: string
staging_draft_validated:
type: boolean
staging_quality_gate_version:
type: string
last_staging_attempt_signature:
type: string
memory_ids:
type: array
items: