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