atlas.go raw

   1  package cartography
   2  
   3  import (
   4  	"encoding/json"
   5  	"fmt"
   6  	"os"
   7  	"sort"
   8  	"strings"
   9  )
  10  
  11  // Atlas is the bidirectional English↔Go map of the entire codebase.
  12  type Atlas struct {
  13  	// All entries, keyed by ID ("package.Name" or "package.Type.Method").
  14  	Entries map[string]*Entry `json:"entries"`
  15  
  16  	// Metadata.
  17  	Generation     int     `json:"generation"`
  18  	TotalEntries   int     `json:"total_entries"`
  19  	MeanConfidence float64 `json:"mean_confidence"`
  20  	OracleCalls    int     `json:"oracle_calls"` // lifetime
  21  
  22  	// In-memory indexes (rebuilt on Load, not serialized).
  23  	byPackage map[string][]*Entry
  24  	byConcept map[string][]*Entry
  25  	byFile    map[string][]*Entry
  26  }
  27  
  28  // NewAtlas creates an empty atlas.
  29  func NewAtlas() *Atlas {
  30  	return &Atlas{
  31  		Entries:   make(map[string]*Entry),
  32  		byPackage: make(map[string][]*Entry),
  33  		byConcept: make(map[string][]*Entry),
  34  		byFile:    make(map[string][]*Entry),
  35  	}
  36  }
  37  
  38  // Add inserts or replaces an entry and updates metadata.
  39  func (a *Atlas) Add(e *Entry) {
  40  	a.Entries[e.ID] = e
  41  	a.updateStats()
  42  }
  43  
  44  // BuildIndexes constructs the in-memory bidirectional indexes from entries.
  45  func (a *Atlas) BuildIndexes() {
  46  	a.byPackage = make(map[string][]*Entry)
  47  	a.byConcept = make(map[string][]*Entry)
  48  	a.byFile = make(map[string][]*Entry)
  49  
  50  	for _, e := range a.Entries {
  51  		a.byPackage[e.Package] = append(a.byPackage[e.Package], e)
  52  		a.byFile[e.FilePath] = append(a.byFile[e.FilePath], e)
  53  		for _, c := range e.Concepts {
  54  			a.byConcept[c] = append(a.byConcept[c], e)
  55  		}
  56  	}
  57  }
  58  
  59  // LookupGo returns the entry for a Go declaration.
  60  // Accepts "package.Name", "package.Type.Method", or just "Name" (searches all).
  61  func (a *Atlas) LookupGo(id string) *Entry {
  62  	// Direct match.
  63  	if e, ok := a.Entries[id]; ok {
  64  		return e
  65  	}
  66  	// Search by bare name.
  67  	for _, e := range a.Entries {
  68  		if e.Name == id {
  69  			return e
  70  		}
  71  	}
  72  	return nil
  73  }
  74  
  75  // LookupEnglish returns Go declarations matching an English query.
  76  // Searches concept tags, descriptions, and doc comments.
  77  // Returns entries sorted by relevance.
  78  func (a *Atlas) LookupEnglish(query string) []*Entry {
  79  	words := splitQuery(query)
  80  	if len(words) == 0 {
  81  		return nil
  82  	}
  83  
  84  	type scored struct {
  85  		entry *Entry
  86  		score float64
  87  	}
  88  	var results []scored
  89  
  90  	for _, e := range a.Entries {
  91  		score := 0.0
  92  		for _, w := range words {
  93  			wl := strings.ToLower(w)
  94  			// Concept match (strongest signal).
  95  			for _, c := range e.Concepts {
  96  				if c == wl {
  97  					score += 3.0
  98  				}
  99  			}
 100  			// Name match.
 101  			if strings.EqualFold(e.Name, w) {
 102  				score += 5.0
 103  			}
 104  			// Package match.
 105  			if strings.EqualFold(e.Package, w) {
 106  				score += 2.0
 107  			}
 108  			// Description match.
 109  			if strings.Contains(strings.ToLower(e.Description), wl) {
 110  				score += 1.0
 111  			}
 112  			// Doc comment match.
 113  			if strings.Contains(strings.ToLower(e.DocComment), wl) {
 114  				score += 0.5
 115  			}
 116  		}
 117  		if score > 0 {
 118  			results = append(results, scored{e, score})
 119  		}
 120  	}
 121  
 122  	sort.Slice(results, func(i, j int) bool {
 123  		return results[i].score > results[j].score
 124  	})
 125  
 126  	entries := make([]*Entry, len(results))
 127  	for i, r := range results {
 128  		entries[i] = r.entry
 129  	}
 130  	return entries
 131  }
 132  
 133  // LookupPackage returns all entries in a package.
 134  func (a *Atlas) LookupPackage(pkg string) []*Entry {
 135  	return a.byPackage[pkg]
 136  }
 137  
 138  // Packages returns all unique package names in the atlas.
 139  func (a *Atlas) Packages() []string {
 140  	pkgs := make([]string, 0, len(a.byPackage))
 141  	for p := range a.byPackage {
 142  		pkgs = append(pkgs, p)
 143  	}
 144  	sort.Strings(pkgs)
 145  	return pkgs
 146  }
 147  
 148  // Summary returns a compact text summary of the atlas suitable for oracle
 149  // context. maxChars limits the output size (0 = unlimited).
 150  // This produces richer context than bare type/function name lists.
 151  func (a *Atlas) Summary(maxChars int) string {
 152  	var b strings.Builder
 153  
 154  	pkgs := a.Packages()
 155  	for _, pkg := range pkgs {
 156  		entries := a.byPackage[pkg]
 157  		if len(entries) == 0 {
 158  			continue
 159  		}
 160  
 161  		// Package header.
 162  		fmt.Fprintf(&b, "Package %s", pkg)
 163  		// Find a package-level description from the highest-confidence entry.
 164  		bestDesc := ""
 165  		for _, e := range entries {
 166  			if e.DocComment != "" && e.Confidence >= 0.3 {
 167  				bestDesc = firstSentence(e.DocComment)
 168  				break
 169  			}
 170  		}
 171  		if bestDesc != "" {
 172  			fmt.Fprintf(&b, " — %s", bestDesc)
 173  		}
 174  		b.WriteString("\n")
 175  
 176  		// Sort entries: types first, then funcs, then methods.
 177  		sorted := make([]*Entry, len(entries))
 178  		copy(sorted, entries)
 179  		sort.Slice(sorted, func(i, j int) bool {
 180  			return kindOrder(sorted[i].Kind) < kindOrder(sorted[j].Kind)
 181  		})
 182  
 183  		for _, e := range sorted {
 184  			if !e.Exported {
 185  				continue // only include exported in summary
 186  			}
 187  			sig := e.Signature
 188  			if sig == "" {
 189  				sig = e.Name
 190  			}
 191  			desc := firstSentence(e.Description)
 192  			if desc != "" {
 193  				fmt.Fprintf(&b, "  %s — %s\n", sig, desc)
 194  			} else {
 195  				fmt.Fprintf(&b, "  %s\n", sig)
 196  			}
 197  
 198  			if maxChars > 0 && b.Len() > maxChars {
 199  				fmt.Fprintf(&b, "  ... (%d more entries)\n", a.TotalEntries-len(a.Entries))
 200  				return b.String()
 201  			}
 202  		}
 203  	}
 204  
 205  	return b.String()
 206  }
 207  
 208  // Merge combines a freshly-extracted atlas with this one.
 209  // New entries are added. Existing entries keep oracle-enriched descriptions
 210  // but update mechanical fields (signature, params, returns) from fresh.
 211  // Entries that no longer exist in fresh are removed.
 212  func (a *Atlas) Merge(fresh *Atlas) {
 213  	// Update existing + add new.
 214  	for id, freshEntry := range fresh.Entries {
 215  		if existing, ok := a.Entries[id]; ok {
 216  			// Keep oracle-enriched fields.
 217  			if existing.Source == "oracle" || existing.Source == "self-revised" {
 218  				freshEntry.Description = existing.Description
 219  				freshEntry.Contract = existing.Contract
 220  				freshEntry.EdgeCases = existing.EdgeCases
 221  				freshEntry.Confidence = existing.Confidence
 222  				freshEntry.Source = existing.Source
 223  				freshEntry.Revision = existing.Revision
 224  				// Keep oracle-enriched param semantics.
 225  				for i := range freshEntry.Params {
 226  					if i < len(existing.Params) && existing.Params[i].Semantic != "" {
 227  						freshEntry.Params[i].Semantic = existing.Params[i].Semantic
 228  					}
 229  				}
 230  				for i := range freshEntry.Returns {
 231  					if i < len(existing.Returns) && existing.Returns[i].Semantic != "" {
 232  						freshEntry.Returns[i].Semantic = existing.Returns[i].Semantic
 233  					}
 234  				}
 235  			}
 236  			// Keep test data if fresh doesn't have it.
 237  			if !freshEntry.HasTest && existing.HasTest {
 238  				freshEntry.HasTest = existing.HasTest
 239  				freshEntry.TestFuncs = existing.TestFuncs
 240  				freshEntry.TestFile = existing.TestFile
 241  				freshEntry.TestPatterns = existing.TestPatterns
 242  				freshEntry.Validation = existing.Validation
 243  			}
 244  		}
 245  		a.Entries[id] = freshEntry
 246  	}
 247  
 248  	// Remove entries not in fresh (code was deleted).
 249  	for id := range a.Entries {
 250  		if _, ok := fresh.Entries[id]; !ok {
 251  			delete(a.Entries, id)
 252  		}
 253  	}
 254  
 255  	a.updateStats()
 256  	a.BuildIndexes()
 257  }
 258  
 259  // Save writes the atlas to a JSON file.
 260  func (a *Atlas) Save(path string) error {
 261  	a.updateStats()
 262  	data, err := json.MarshalIndent(a, "", "  ")
 263  	if err != nil {
 264  		return err
 265  	}
 266  	return os.WriteFile(path, data, 0o644)
 267  }
 268  
 269  // Load reads an atlas from a JSON file and rebuilds indexes.
 270  func Load(path string) (*Atlas, error) {
 271  	data, err := os.ReadFile(path)
 272  	if err != nil {
 273  		return nil, err
 274  	}
 275  	a := &Atlas{}
 276  	if err := json.Unmarshal(data, a); err != nil {
 277  		return nil, err
 278  	}
 279  	if a.Entries == nil {
 280  		a.Entries = make(map[string]*Entry)
 281  	}
 282  	a.BuildIndexes()
 283  	return a, nil
 284  }
 285  
 286  // LowestConfidence returns the n entries with the lowest confidence,
 287  // sorted ascending. Ties are broken by ID for deterministic ordering.
 288  func (a *Atlas) LowestConfidence(n int) []*Entry {
 289  	all := make([]*Entry, 0, len(a.Entries))
 290  	for _, e := range a.Entries {
 291  		all = append(all, e)
 292  	}
 293  	sort.Slice(all, func(i, j int) bool {
 294  		if all[i].Confidence != all[j].Confidence {
 295  			return all[i].Confidence < all[j].Confidence
 296  		}
 297  		return all[i].ID < all[j].ID
 298  	})
 299  	if n > len(all) {
 300  		n = len(all)
 301  	}
 302  	return all[:n]
 303  }
 304  
 305  func (a *Atlas) updateStats() {
 306  	a.TotalEntries = len(a.Entries)
 307  	if a.TotalEntries == 0 {
 308  		a.MeanConfidence = 0
 309  		return
 310  	}
 311  	total := 0.0
 312  	for _, e := range a.Entries {
 313  		total += e.Confidence
 314  	}
 315  	a.MeanConfidence = total / float64(a.TotalEntries)
 316  }
 317  
 318  // splitQuery breaks an English query into searchable words.
 319  func splitQuery(query string) []string {
 320  	words := strings.Fields(strings.ToLower(query))
 321  	// Filter stop words.
 322  	var result []string
 323  	for _, w := range words {
 324  		if len(w) >= 3 && !stopWords[w] {
 325  			result = append(result, w)
 326  		}
 327  	}
 328  	return result
 329  }
 330  
 331  var stopWords = map[string]bool{
 332  	"the": true, "and": true, "for": true, "are": true,
 333  	"but": true, "not": true, "you": true, "all": true,
 334  	"can": true, "had": true, "her": true, "was": true,
 335  	"one": true, "our": true, "out": true, "has": true,
 336  	"its": true, "that": true, "with": true, "this": true,
 337  	"from": true, "they": true, "been": true, "have": true,
 338  	"which": true, "when": true, "what": true, "does": true,
 339  	"how": true, "where": true,
 340  }
 341  
 342  // firstSentence returns the first sentence of a string (up to first period
 343  // or newline), trimmed.
 344  func firstSentence(s string) string {
 345  	s = strings.TrimSpace(s)
 346  	if i := strings.IndexByte(s, '.'); i >= 0 && i < 120 {
 347  		return s[:i+1]
 348  	}
 349  	if i := strings.IndexByte(s, '\n'); i >= 0 {
 350  		return strings.TrimSpace(s[:i])
 351  	}
 352  	if len(s) > 120 {
 353  		return s[:120] + "..."
 354  	}
 355  	return s
 356  }
 357  
 358  func kindOrder(kind string) int {
 359  	switch kind {
 360  	case "type", "interface":
 361  		return 0
 362  	case "func":
 363  		return 1
 364  	case "method":
 365  		return 2
 366  	default:
 367  		return 3
 368  	}
 369  }
 370