|
| 1 | +package review |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "strings" |
| 6 | + |
| 7 | + "github.com/randomcodespace/codeiq/go/internal/graph" |
| 8 | +) |
| 9 | + |
| 10 | +// KuzuGraphContext implements GraphContext by querying an open Kuzu store. |
| 11 | +// Wire from CLI: open store, NewKuzuGraphContext(store), pass to NewService. |
| 12 | +// |
| 13 | +// EvidenceForFile returns a compact textual summary that the LLM finds |
| 14 | +// useful: nodes-in-file with kind + layer, plus 1-hop blast radius node |
| 15 | +// IDs. Strictly read-only. |
| 16 | +type KuzuGraphContext struct { |
| 17 | + Store *graph.Store |
| 18 | +} |
| 19 | + |
| 20 | +// NewKuzuGraphContext returns a context backed by store. nil Store yields |
| 21 | +// empty evidence (the LLM degrades gracefully to diff-only review). |
| 22 | +func NewKuzuGraphContext(store *graph.Store) *KuzuGraphContext { |
| 23 | + return &KuzuGraphContext{Store: store} |
| 24 | +} |
| 25 | + |
| 26 | +// EvidenceForFile satisfies GraphContext. Empty string when the store is |
| 27 | +// missing or the file has no graph nodes. |
| 28 | +func (k *KuzuGraphContext) EvidenceForFile(path string) string { |
| 29 | + if k == nil || k.Store == nil || path == "" { |
| 30 | + return "" |
| 31 | + } |
| 32 | + rows, err := k.Store.Cypher(` |
| 33 | + MATCH (n:CodeNode) WHERE n.file_path = $f |
| 34 | + RETURN n.id AS id, n.kind AS kind, n.label AS label, n.layer AS layer |
| 35 | + ORDER BY n.id LIMIT 25`, map[string]any{"f": path}) |
| 36 | + if err != nil || len(rows) == 0 { |
| 37 | + return "" |
| 38 | + } |
| 39 | + var b strings.Builder |
| 40 | + fmt.Fprintf(&b, "%d node(s) defined in this file:\n", len(rows)) |
| 41 | + for _, r := range rows { |
| 42 | + id, _ := r["id"].(string) |
| 43 | + kind, _ := r["kind"].(string) |
| 44 | + label, _ := r["label"].(string) |
| 45 | + layer, _ := r["layer"].(string) |
| 46 | + fmt.Fprintf(&b, "- [%s/%s] %s (%s)\n", kind, layer, label, id) |
| 47 | + } |
| 48 | + // 1-hop blast radius: who depends on these nodes? |
| 49 | + ids := make([]any, 0, len(rows)) |
| 50 | + for _, r := range rows { |
| 51 | + if id, ok := r["id"].(string); ok { |
| 52 | + ids = append(ids, id) |
| 53 | + } |
| 54 | + } |
| 55 | + if len(ids) == 0 { |
| 56 | + return b.String() |
| 57 | + } |
| 58 | + deps, err := k.Store.Cypher(` |
| 59 | + MATCH (caller:CodeNode)-[r]->(target:CodeNode) |
| 60 | + WHERE target.id IN $ids |
| 61 | + RETURN DISTINCT caller.id AS id, caller.kind AS kind, caller.label AS label |
| 62 | + ORDER BY caller.id LIMIT 15`, map[string]any{"ids": ids}) |
| 63 | + if err == nil && len(deps) > 0 { |
| 64 | + fmt.Fprintf(&b, "\nBlast radius (1 hop, upstream callers): %d node(s)\n", len(deps)) |
| 65 | + for _, d := range deps { |
| 66 | + id, _ := d["id"].(string) |
| 67 | + kind, _ := d["kind"].(string) |
| 68 | + label, _ := d["label"].(string) |
| 69 | + fmt.Fprintf(&b, "- [%s] %s (%s)\n", kind, label, id) |
| 70 | + } |
| 71 | + } |
| 72 | + return b.String() |
| 73 | +} |
0 commit comments