diff --git a/src/go/memory-consolidation/gaps.go b/src/go/memory-consolidation/gaps.go index ff51e3b5..7a020f9a 100644 --- a/src/go/memory-consolidation/gaps.go +++ b/src/go/memory-consolidation/gaps.go @@ -14,8 +14,10 @@ import ( "fmt" "math" "os" + "path/filepath" "sort" "strings" + "sync" "time" ) @@ -33,6 +35,13 @@ type Compressor func(entry map[string]any) string // the oracle fails silent (entry_id None) — nil lookup keeps that shape. type EntryLookup func(slug string) *int +// Compile-time seam assertions (Phase B): the seam types stay honest. +var ( + _ Clock = frozenClock + _ Compressor = compressEntry + _ EntryLookup = EntryLookup(nil) +) + // frozenClock pins MC_FROZEN_NOW (scripts/mc_freeze_seams/sitecustomize.py) // — the instant the frozen truth was captured at. var frozenClock Clock = func() time.Time { @@ -244,27 +253,11 @@ func consolidateMemory(memoryText string, score float64, washCount int, entryID // word overlap + tag overlap*2 + confidence*3, score>0 gate, stable // descending sort, limit cut. func getRelevantKnowledge(query string, limit int, knowledge []map[string]any) []map[string]any { - qw := map[string]bool{} - for _, w := range strings.Fields(strings.ToLower(query)) { - qw[w] = true - } - type scored struct { - s float64 - k map[string]any - } - var sc []scored - for _, k := range knowledge { - cw := map[string]bool{} - content, _ := k["content"].(string) - for _, w := range strings.Fields(strings.ToLower(content)) { - cw[w] = true - } - overlap := 0 - for w := range cw { - if qw[w] { - overlap++ - } - } + qw := wordSet(query) + return scoredTop(knowledge, limit, func(k map[string]any) (float64, bool) { + cw := wordSet(asString(k["content"])) + overlap := len(intersect(qw, cw)) + tagOverlap := 0 seen := map[string]bool{} if tags, ok := k["tags"].([]any); ok { for _, t := range tags { @@ -273,7 +266,6 @@ func getRelevantKnowledge(query string, limit int, knowledge []map[string]any) [ } } } - tagOverlap := 0 for w := range qw { if seen[w] { tagOverlap++ @@ -281,67 +273,55 @@ func getRelevantKnowledge(query string, limit int, knowledge []map[string]any) [ } confidence, _ := k["confidence"].(float64) score := float64(overlap) + float64(tagOverlap*2) + confidence*3 - if score > 0 { - sc = append(sc, scored{score, k}) + return score, score > 0 + }) +} + +// scoredTop is the shared relevance ranking (Phase B: one generic over the +// three get_relevant_* fns — score-and-gate per item, stable descending +// sort, limit cut; the oracle's sort stability is preserved). +func scoredTop[T any](items []T, limit int, score func(T) (float64, bool)) []T { + type pair struct { + s float64 + it T + } + var ps []pair + for _, it := range items { + if s, ok := score(it); ok { + ps = append(ps, pair{s, it}) } } - sort.SliceStable(sc, func(i, j int) bool { return sc[i].s > sc[j].s }) - out := make([]map[string]any, 0) - for i := 0; i < len(sc) && i < limit; i++ { - out = append(out, sc[i].k) + sort.SliceStable(ps, func(i, j int) bool { return ps[i].s > ps[j].s }) + out := make([]T, 0, min(limit, len(ps))) + for i := 0; i < len(ps) && i < limit; i++ { + out = append(out, ps[i].it) } return out } +func asString(v any) string { s, _ := v.(string); return s } + // getRelevantLessons mirrors MemoryConsolidation.get_relevant_lessons: // trigger+lesson word overlap, overlap>0 gate, stable descending sort, // limit cut. func getRelevantLessons(query string, limit int, lessons []map[string]any) []map[string]any { qw := wordSet(query) - type scored struct { - n int - l map[string]any - } - var sc []scored - for _, l := range lessons { - trigger, _ := l["trigger"].(string) - lesson, _ := l["lesson"].(string) - overlap := len(intersect(qw, wordSet(trigger))) + len(intersect(qw, wordSet(lesson))) - if overlap > 0 { - sc = append(sc, scored{overlap, l}) - } - } - sort.SliceStable(sc, func(i, j int) bool { return sc[i].n > sc[j].n }) - out := make([]map[string]any, 0) - for i := 0; i < len(sc) && i < limit; i++ { - out = append(out, sc[i].l) - } - return out + return scoredTop(lessons, limit, func(l map[string]any) (float64, bool) { + overlap := len(intersect(qw, wordSet(asString(l["trigger"])))) + + len(intersect(qw, wordSet(asString(l["lesson"])))) + return float64(overlap), overlap > 0 + }) } // getRelevantConnections mirrors MemoryConsolidation.get_relevant_connections: // from_concept+to_concept word overlap, overlap>0 gate, stable sort, limit. func getRelevantConnections(query string, limit int, connections []map[string]any) []map[string]any { qw := wordSet(query) - type scored struct { - n int - c map[string]any - } - var sc []scored - for _, c := range connections { - from, _ := c["from_concept"].(string) - to, _ := c["to_concept"].(string) - overlap := len(intersect(qw, wordSet(from))) + len(intersect(qw, wordSet(to))) - if overlap > 0 { - sc = append(sc, scored{overlap, c}) - } - } - sort.SliceStable(sc, func(i, j int) bool { return sc[i].n > sc[j].n }) - out := make([]map[string]any, 0) - for i := 0; i < len(sc) && i < limit; i++ { - out = append(out, sc[i].c) - } - return out + return scoredTop(connections, limit, func(c map[string]any) (float64, bool) { + overlap := len(intersect(qw, wordSet(asString(c["from_concept"])))) + + len(intersect(qw, wordSet(asString(c["to_concept"])))) + return float64(overlap), overlap > 0 + }) } // wordSet mirrors Python set(s.lower().split()) (whitespace-run split). @@ -455,3 +435,18 @@ func saveJSONList(path string, items []map[string]any) { } _ = os.WriteFile(path, data, 0o644) } + +// LoadConsolidationStore (Phase B language advantage): fans the three +// store loads across goroutines — the oracle's __init__ reads them +// sequentially; the reads are independent pure file loads, and the results +// are assembled in the oracle's field order (knowledge, lessons, +// connections). +func LoadConsolidationStore(dir string) (knowledge, lessons, connections []map[string]any) { + var wg sync.WaitGroup + wg.Add(3) + go func() { defer wg.Done(); knowledge = LoadKnowledge(filepath.Join(dir, "knowledge.json")) }() + go func() { defer wg.Done(); lessons = LoadLessons(filepath.Join(dir, "lessons.json")) }() + go func() { defer wg.Done(); connections = LoadConnections(filepath.Join(dir, "connections.json")) }() + wg.Wait() + return knowledge, lessons, connections +} diff --git a/src/go/memory-consolidation/gaps_test.go b/src/go/memory-consolidation/gaps_test.go index eb161797..a76ac8c0 100644 --- a/src/go/memory-consolidation/gaps_test.go +++ b/src/go/memory-consolidation/gaps_test.go @@ -11,6 +11,7 @@ package main import ( "encoding/json" "os" + "path/filepath" "testing" ) @@ -80,3 +81,22 @@ func TestGapStatusShape(t *testing.T) { t.Errorf("status dir: got %v", st["consolidation_dir"]) } } + +// TestGapConcurrentStoreLoad pins the Phase B fan-out: three independent +// file loads raced, assembled in oracle field order. +func TestGapConcurrentStoreLoad(t *testing.T) { + dir := t.TempDir() + SaveKnowledge(filepath.Join(dir, "knowledge.json"), []map[string]any{{"id": "k1"}}) + SaveLessons(filepath.Join(dir, "lessons.json"), []map[string]any{{"id": "lesson-0"}}) + SaveConnections(filepath.Join(dir, "connections.json"), nil) + k, l, c := LoadConsolidationStore(dir) + if len(k) != 1 || k[0]["id"] != "k1" { + t.Errorf("knowledge fan-out: got %v", k) + } + if len(l) != 1 || l[0]["id"] != "lesson-0" { + t.Errorf("lessons fan-out: got %v", l) + } + if len(c) != 0 { + t.Errorf("connections fan-out (absent file): got %v", c) + } +}