• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

valksor / kvelmo / 23471163553

24 Mar 2026 02:51AM UTC coverage: 49.867% (-1.4%) from 51.3%
23471163553

push

github

k0d3r1s
Update project config and gap analysis commands

Update CLAUDE.md with new CLI commands and package descriptions. Revise
AGENTS.md with current architecture guidance. Add CodeRabbit config
rule. Update lefthook pre-commit hook. Refresh all gap analysis
commands with current feature inventory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

811 of 1374 branches covered (59.02%)

Branch coverage included in aggregate %.

22001 of 44372 relevant lines covered (49.58%)

0.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

62.11
/pkg/codegraph/graph.go
1
package codegraph
2

3
import (
4
        "context"
5
        "database/sql"
6
        "fmt"
7
        "log/slog"
8
        "os"
9
        "path/filepath"
10
        "strings"
11

12
        _ "modernc.org/sqlite"
13
)
14

15
// Graph stores code symbols and relationships in SQLite.
16
type Graph struct {
17
        db      *sql.DB
18
        rootDir string
19
}
20

21
// Symbol represents a code symbol (function, type, interface, method).
22
type Symbol struct {
23
        ID      int64  `json:"id"`
24
        Name    string `json:"name"`
25
        Kind    string `json:"kind"` // "function", "type", "interface", "method", "const", "var"
26
        File    string `json:"file"`
27
        Line    int    `json:"line"`
28
        Package string `json:"package"`
29
}
30

31
// Edge represents a relationship between symbols.
32
type Edge struct {
33
        FromID   int64  `json:"from_id"`
34
        ToID     int64  `json:"to_id"`
35
        Relation string `json:"relation"` // "calls", "implements", "embeds", "references"
36
}
37

38
const schema = `
39
CREATE TABLE IF NOT EXISTS symbols (
40
    id INTEGER PRIMARY KEY AUTOINCREMENT,
41
    name TEXT NOT NULL,
42
    kind TEXT NOT NULL,
43
    file TEXT NOT NULL,
44
    line INTEGER NOT NULL,
45
    package TEXT NOT NULL
46
);
47
CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
48
CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file);
49
CREATE INDEX IF NOT EXISTS idx_symbols_package ON symbols(package);
50

51
CREATE TABLE IF NOT EXISTS edges (
52
    from_id INTEGER NOT NULL REFERENCES symbols(id),
53
    to_id INTEGER NOT NULL REFERENCES symbols(id),
54
    relation TEXT NOT NULL
55
);
56
CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_id);
57
CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_id);
58
`
59

60
// New opens or creates a SQLite-backed code graph at dbPath.
61
func New(ctx context.Context, dbPath string) (*Graph, error) {
1✔
62
        if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil {
1✔
63
                return nil, fmt.Errorf("create db directory: %w", err)
×
64
        }
×
65

66
        db, err := sql.Open("sqlite", dbPath)
1✔
67
        if err != nil {
1✔
68
                return nil, fmt.Errorf("open sqlite: %w", err)
×
69
        }
×
70

71
        // Enable WAL mode for better concurrent read performance.
72
        if _, err := db.ExecContext(ctx, "PRAGMA journal_mode=WAL"); err != nil {
1✔
73
                _ = db.Close()
×
74

×
75
                return nil, fmt.Errorf("set WAL mode: %w", err)
×
76
        }
×
77

78
        // Enable foreign key constraints for referential integrity.
79
        if _, err := db.ExecContext(ctx, "PRAGMA foreign_keys=ON"); err != nil {
1✔
80
                _ = db.Close()
×
81

×
82
                return nil, fmt.Errorf("enable foreign keys: %w", err)
×
83
        }
×
84

85
        if _, err := db.ExecContext(ctx, schema); err != nil {
1✔
86
                _ = db.Close()
×
87

×
88
                return nil, fmt.Errorf("create schema: %w", err)
×
89
        }
×
90

91
        // rootDir is left empty here; it is set by IndexDirectory() to enable
92
        // relative path storage. Calling IndexFile() without IndexDirectory()
93
        // stores absolute paths, which is valid but less portable.
94
        return &Graph{db: db}, nil
1✔
95
}
96

97
// Close closes the underlying database connection.
98
func (g *Graph) Close() error {
1✔
99
        if g.db == nil {
1✔
100
                return nil
×
101
        }
×
102

103
        return g.db.Close()
1✔
104
}
105

106
// IndexFile parses a single Go file and indexes its symbols and edges.
107
func (g *Graph) IndexFile(ctx context.Context, path string) error {
1✔
108
        absPath, err := filepath.Abs(path)
1✔
109
        if err != nil {
1✔
110
                return fmt.Errorf("abs path: %w", err)
×
111
        }
×
112

113
        // Compute relative path for storage if rootDir is set.
114
        storePath := absPath
1✔
115
        if g.rootDir != "" {
2✔
116
                if rel, relErr := filepath.Rel(g.rootDir, absPath); relErr == nil {
2✔
117
                        storePath = rel
1✔
118
                }
1✔
119
        }
120

121
        symbols, edges, err := parseGoFile(absPath)
1✔
122
        if err != nil {
1✔
123
                return fmt.Errorf("parse %s: %w", path, err)
×
124
        }
×
125

126
        tx, err := g.db.BeginTx(ctx, nil)
1✔
127
        if err != nil {
1✔
128
                return fmt.Errorf("begin tx: %w", err)
×
129
        }
×
130
        defer tx.Rollback() //nolint:errcheck // rollback after commit is harmless
1✔
131

1✔
132
        // Remove old symbols and edges for this file.
1✔
133
        if _, err := tx.ExecContext(ctx, "DELETE FROM edges WHERE from_id IN (SELECT id FROM symbols WHERE file = ?) OR to_id IN (SELECT id FROM symbols WHERE file = ?)", storePath, storePath); err != nil {
1✔
134
                return fmt.Errorf("delete old edges: %w", err)
×
135
        }
×
136
        if _, err := tx.ExecContext(ctx, "DELETE FROM symbols WHERE file = ?", storePath); err != nil {
1✔
137
                return fmt.Errorf("delete old symbols: %w", err)
×
138
        }
×
139

140
        // Insert symbols and build a name->id map for edge resolution.
141
        nameToID := make(map[string]int64)
1✔
142
        for i := range symbols {
2✔
143
                symbols[i].File = storePath
1✔
144
                res, err := tx.ExecContext(ctx,
1✔
145
                        "INSERT INTO symbols (name, kind, file, line, package) VALUES (?, ?, ?, ?, ?)",
1✔
146
                        symbols[i].Name, symbols[i].Kind, symbols[i].File, symbols[i].Line, symbols[i].Package,
1✔
147
                )
1✔
148
                if err != nil {
1✔
149
                        return fmt.Errorf("insert symbol %s: %w", symbols[i].Name, err)
×
150
                }
×
151
                id, err := res.LastInsertId()
1✔
152
                if err != nil {
1✔
153
                        return fmt.Errorf("get last insert id for %s: %w", symbols[i].Name, err)
×
154
                }
×
155
                symbols[i].ID = id
1✔
156
                nameToID[symbols[i].Name] = id
1✔
157
        }
158

159
        // Insert edges — resolve names to IDs within this file, or look up globally.
160
        for _, e := range edges {
2✔
161
                fromID, ok := nameToID[e.FromName]
1✔
162
                if !ok {
2✔
163
                        continue
1✔
164
                }
165

166
                toID, ok := nameToID[e.ToName]
1✔
167
                if !ok {
2✔
168
                        // Try to find the target symbol in the database (from other files).
1✔
169
                        row := tx.QueryRowContext(ctx, "SELECT id FROM symbols WHERE name = ? LIMIT 1", e.ToName)
1✔
170
                        if err := row.Scan(&toID); err != nil {
2✔
171
                                continue // Skip unresolved edges.
1✔
172
                        }
173
                }
174

175
                if _, err := tx.ExecContext(ctx,
×
176
                        "INSERT INTO edges (from_id, to_id, relation) VALUES (?, ?, ?)",
×
177
                        fromID, toID, e.Relation,
×
178
                ); err != nil {
×
179
                        return fmt.Errorf("insert edge: %w", err)
×
180
                }
×
181
        }
182

183
        return tx.Commit()
1✔
184
}
185

186
// IndexDirectory walks dir and indexes all .go files (excluding vendor, testdata).
187
func (g *Graph) IndexDirectory(ctx context.Context, dir string) error {
1✔
188
        absDir, err := filepath.Abs(dir)
1✔
189
        if err != nil {
1✔
190
                return fmt.Errorf("abs dir: %w", err)
×
191
        }
×
192
        g.rootDir = absDir
1✔
193

1✔
194
        var indexed int
1✔
195
        walkErr := filepath.WalkDir(absDir, func(path string, d os.DirEntry, walkDirErr error) error {
2✔
196
                if walkDirErr != nil {
1✔
197
                        slog.Debug("skipping inaccessible entry", "path", path, "error", walkDirErr)
×
198

×
199
                        return filepath.SkipDir
×
200
                }
×
201

202
                name := d.Name()
1✔
203

1✔
204
                // Skip common non-source directories.
1✔
205
                if d.IsDir() {
2✔
206
                        switch name {
1✔
207
                        case "vendor", "testdata", "node_modules", ".git":
×
208
                                return filepath.SkipDir
×
209
                        }
210

211
                        return nil
1✔
212
                }
213

214
                if !strings.HasSuffix(name, ".go") {
2✔
215
                        return nil
1✔
216
                }
1✔
217

218
                if err := g.IndexFile(ctx, path); err != nil {
1✔
219
                        slog.Debug("skipping file", "path", path, "error", err)
×
220

×
221
                        return nil
×
222
                }
×
223
                indexed++
1✔
224

1✔
225
                return nil
1✔
226
        })
227
        if walkErr != nil {
1✔
228
                return fmt.Errorf("walk directory: %w", walkErr)
×
229
        }
×
230

231
        slog.Info("code graph indexed", "dir", dir, "files", indexed)
1✔
232

1✔
233
        return nil
1✔
234
}
235

236
// QueryCallersOf finds symbols that call the given function name.
237
func (g *Graph) QueryCallersOf(ctx context.Context, name string) ([]Symbol, error) {
1✔
238
        rows, err := g.db.QueryContext(ctx, `
1✔
239
                SELECT s.id, s.name, s.kind, s.file, s.line, s.package
1✔
240
                FROM symbols s
1✔
241
                JOIN edges e ON e.from_id = s.id
1✔
242
                JOIN symbols target ON e.to_id = target.id
1✔
243
                WHERE target.name = ? AND e.relation = 'calls'
1✔
244
        `, name)
1✔
245
        if err != nil {
1✔
246
                return nil, fmt.Errorf("query callers: %w", err)
×
247
        }
×
248
        defer func() { _ = rows.Close() }()
2✔
249

250
        return scanSymbols(rows)
1✔
251
}
252

253
// QueryDependenciesOf finds packages imported by the given package.
254
func (g *Graph) QueryDependenciesOf(ctx context.Context, pkg string) ([]string, error) {
×
255
        rows, err := g.db.QueryContext(ctx, `
×
256
                SELECT DISTINCT target.package
×
257
                FROM symbols s
×
258
                JOIN edges e ON e.from_id = s.id
×
259
                JOIN symbols target ON e.to_id = target.id
×
260
                WHERE s.package = ? AND e.relation = 'imports'
×
261
        `, pkg)
×
262
        if err != nil {
×
263
                return nil, fmt.Errorf("query dependencies: %w", err)
×
264
        }
×
265
        defer func() { _ = rows.Close() }()
×
266

267
        var deps []string
×
268
        for rows.Next() {
×
269
                var dep string
×
270
                if err := rows.Scan(&dep); err != nil {
×
271
                        return nil, fmt.Errorf("scan dependency: %w", err)
×
272
                }
×
273
                deps = append(deps, dep)
×
274
        }
275

276
        return deps, rows.Err()
×
277
}
278

279
// QuerySymbol finds symbols matching the given name (exact match).
280
func (g *Graph) QuerySymbol(ctx context.Context, name string) ([]Symbol, error) {
1✔
281
        rows, err := g.db.QueryContext(ctx,
1✔
282
                "SELECT id, name, kind, file, line, package FROM symbols WHERE name = ?",
1✔
283
                name,
1✔
284
        )
1✔
285
        if err != nil {
1✔
286
                return nil, fmt.Errorf("query symbol: %w", err)
×
287
        }
×
288
        defer func() { _ = rows.Close() }()
2✔
289

290
        return scanSymbols(rows)
1✔
291
}
292

293
// QuerySymbolPattern finds symbols matching a LIKE pattern (use % for wildcards).
294
func (g *Graph) QuerySymbolPattern(ctx context.Context, pattern string) ([]Symbol, error) {
1✔
295
        rows, err := g.db.QueryContext(ctx,
1✔
296
                "SELECT id, name, kind, file, line, package FROM symbols WHERE name LIKE ?",
1✔
297
                pattern,
1✔
298
        )
1✔
299
        if err != nil {
1✔
300
                return nil, fmt.Errorf("query symbol pattern: %w", err)
×
301
        }
×
302
        defer func() { _ = rows.Close() }()
2✔
303

304
        return scanSymbols(rows)
1✔
305
}
306

307
// Stats returns counts of symbols and edges by kind/relation.
308
func (g *Graph) Stats(ctx context.Context) map[string]int {
1✔
309
        stats := make(map[string]int)
1✔
310

1✔
311
        // Total symbols.
1✔
312
        row := g.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM symbols")
1✔
313
        var count int
1✔
314
        if err := row.Scan(&count); err == nil {
2✔
315
                stats["symbols"] = count
1✔
316
        }
1✔
317

318
        // Symbols by kind.
319
        if err := g.collectGroupCounts(ctx, "SELECT kind, COUNT(*) FROM symbols GROUP BY kind", "symbols_", stats); err != nil {
1✔
320
                slog.Debug("stats: symbols by kind", "error", err)
×
321
        }
×
322

323
        // Total edges.
324
        row = g.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM edges")
1✔
325
        if err := row.Scan(&count); err == nil {
2✔
326
                stats["edges"] = count
1✔
327
        }
1✔
328

329
        // Edges by relation.
330
        if err := g.collectGroupCounts(ctx, "SELECT relation, COUNT(*) FROM edges GROUP BY relation", "edges_", stats); err != nil {
1✔
331
                slog.Debug("stats: edges by relation", "error", err)
×
332
        }
×
333

334
        // File count.
335
        row = g.db.QueryRowContext(ctx, "SELECT COUNT(DISTINCT file) FROM symbols")
1✔
336
        if err := row.Scan(&count); err == nil {
2✔
337
                stats["files"] = count
1✔
338
        }
1✔
339

340
        return stats
1✔
341
}
342

343
// collectGroupCounts runs a "SELECT label, COUNT(*)" query and stores results in stats with the given prefix.
344
func (g *Graph) collectGroupCounts(ctx context.Context, query, prefix string, stats map[string]int) error {
1✔
345
        rows, err := g.db.QueryContext(ctx, query)
1✔
346
        if err != nil {
1✔
347
                return fmt.Errorf("query: %w", err)
×
348
        }
×
349
        defer func() { _ = rows.Close() }()
2✔
350

351
        for rows.Next() {
2✔
352
                var label string
1✔
353
                var c int
1✔
354
                if err := rows.Scan(&label, &c); err == nil {
2✔
355
                        stats[prefix+label] = c
1✔
356
                }
1✔
357
        }
358

359
        return rows.Err()
1✔
360
}
361

362
func scanSymbols(rows *sql.Rows) ([]Symbol, error) {
1✔
363
        var symbols []Symbol
1✔
364
        for rows.Next() {
2✔
365
                var s Symbol
1✔
366
                if err := rows.Scan(&s.ID, &s.Name, &s.Kind, &s.File, &s.Line, &s.Package); err != nil {
1✔
367
                        return nil, fmt.Errorf("scan symbol: %w", err)
×
368
                }
×
369
                symbols = append(symbols, s)
1✔
370
        }
371

372
        return symbols, rows.Err()
1✔
373
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc