Files
2026-07-13 13:00:08 +08:00

135 lines
4.3 KiB
Go

package memorycompiler
import (
"fmt"
"sort"
"strings"
"time"
)
// StrategyReport summarizes one strategy's learned outcome counters, split by
// whether the compiled contract was provider-visible that turn.
type StrategyReport struct {
ID string
Description string
Successes int
Failures int
InjectedSuccesses int
InjectedFailures int
LastUsedAt time.Time
}
func (s StrategyReport) samples() int { return s.Successes + s.Failures }
func (s StrategyReport) injectedSamples() int { return s.InjectedSuccesses + s.InjectedFailures }
// LearningsReport is a read-only, human-readable snapshot of what Memory v5
// has learned for one project. It exposes local state only; nothing here is
// sent to a provider.
type LearningsReport struct {
UpdatedAt time.Time
Strategies []StrategyReport
NoisePatterns []string
Improvements []string
Findings []string
Nodes int
HighSignalNodes int
}
// LearningsReport loads the project's learned state. ok is false when nothing
// has been learned yet. maxItems bounds each list; <=0 uses a default.
func (r *Runtime) LearningsReport(maxItems int) (LearningsReport, bool) {
if r == nil {
return LearningsReport{}, false
}
if maxItems <= 0 {
maxItems = 8
}
st := r.loadState()
rep := LearningsReport{UpdatedAt: st.UpdatedAt, Nodes: len(st.Nodes)}
for _, node := range st.Nodes {
if node.Quality == QualityHighSignal {
rep.HighSignalNodes++
}
}
for _, s := range st.Strategies {
if s.Samples() == 0 {
continue
}
rep.Strategies = append(rep.Strategies, StrategyReport{
ID: s.ID,
Description: s.Description,
Successes: s.Successes,
Failures: s.Failures,
InjectedSuccesses: s.InjectedSuccesses,
InjectedFailures: s.InjectedFailures,
LastUsedAt: s.LastUsedAt,
})
}
sort.Slice(rep.Strategies, func(i, j int) bool {
if rep.Strategies[i].samples() == rep.Strategies[j].samples() {
return rep.Strategies[i].ID < rep.Strategies[j].ID
}
return rep.Strategies[i].samples() > rep.Strategies[j].samples()
})
for _, ref := range sortedNoisyRefs(st.NoisyRefs) {
rep.NoisePatterns = append(rep.NoisePatterns, fmt.Sprintf("%s (x%d)", ref.ref, ref.count))
}
rep.NoisePatterns = limitStrings(rep.NoisePatterns, maxItems)
for _, l := range recentLearnings(st.Learnings, maxItems) {
rep.Improvements = append(rep.Improvements, l.CompilerImprovements...)
rep.Findings = append(rep.Findings, l.CausalFindings...)
}
rep.Improvements = limitStrings(dedupeStrings(rep.Improvements), maxItems)
rep.Findings = limitStrings(dedupeStrings(rep.Findings), maxItems)
ok := len(rep.Strategies)+len(rep.NoisePatterns)+len(rep.Improvements)+len(rep.Findings) > 0
return rep, ok
}
// FormatLearningsReport renders the report as plain notice text for the CLI and
// desktop slash-command surfaces.
func FormatLearningsReport(rep LearningsReport) string {
var b strings.Builder
b.WriteString("memory-v5 learnings")
if !rep.UpdatedAt.IsZero() {
fmt.Fprintf(&b, " (updated %s)", rep.UpdatedAt.UTC().Format("2006-01-02 15:04 UTC"))
}
if rep.Nodes > 0 {
fmt.Fprintf(&b, "\nmemory nodes: %d (%d high-signal)", rep.Nodes, rep.HighSignalNodes)
}
if len(rep.Strategies) > 0 {
b.WriteString("\nstrategies:")
for _, s := range rep.Strategies {
fmt.Fprintf(&b, "\n %s: %s", s.ID, formatOutcomeSplit(s.Successes, s.Failures))
if s.injectedSamples() > 0 {
observedOK := s.Successes - s.InjectedSuccesses
observedFail := s.Failures - s.InjectedFailures
fmt.Fprintf(&b, " | injected %s vs observed %s",
formatOutcomeSplit(s.InjectedSuccesses, s.InjectedFailures),
formatOutcomeSplit(observedOK, observedFail))
}
}
}
writeReportList(&b, "repeated noise patterns", rep.NoisePatterns)
writeReportList(&b, "recent improvements", rep.Improvements)
writeReportList(&b, "recent findings", rep.Findings)
return b.String()
}
func formatOutcomeSplit(ok, fail int) string {
total := ok + fail
if total == 0 {
return "0 samples"
}
return fmt.Sprintf("%d ok / %d fail (%d%%)", ok, fail, int(float64(ok)/float64(total)*100+0.5))
}
func writeReportList(b *strings.Builder, title string, items []string) {
if len(items) == 0 {
return
}
b.WriteString("\n" + title + ":")
for _, item := range items {
b.WriteString("\n - " + summarizeText(item, 160))
}
}