Skip to content

Commit 621b0b4

Browse files
docs(eval): annotate evaluation reports and queries
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent a2e84f8 commit 621b0b4

6 files changed

Lines changed: 62 additions & 0 deletions

File tree

internal/cli/eval.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// @index CLI command for parser and search evaluation against golden corpora.
12
package cli
23

34
import (
@@ -13,6 +14,7 @@ import (
1314
"github.com/tae2089/code-context-graph/internal/parse/treesitter"
1415
)
1516

17+
// @intent expose parser and search evaluation workflows from the CLI with one shared command surface.
1618
func newEvalCmd(deps *Deps) *cobra.Command {
1719
var corpusDir string
1820
var suite string
@@ -54,6 +56,7 @@ func newEvalCmd(deps *Deps) *cobra.Command {
5456
return cmd
5557
}
5658

59+
// @intent adapt the configured search backend to the eval package's simple search callback contract.
5760
func makeSearchFn(ctx context.Context, deps *Deps) eval.SearchFunc {
5861
return func(query string, limit int) ([]string, error) {
5962
if deps.DB == nil || deps.SearchBackend == nil {
@@ -68,6 +71,7 @@ func makeSearchFn(ctx context.Context, deps *Deps) eval.SearchFunc {
6871
}
6972

7073
// extToLangWalkers converts extension-keyed walkers (e.g. ".go") to language-keyed (e.g. "go").
74+
// @intent normalize parser walker lookup so eval suites can address languages consistently.
7175
func extToLangWalkers(walkers map[string]*treesitter.Walker) map[string]*treesitter.Walker {
7276
if walkers == nil {
7377
return nil
@@ -87,6 +91,7 @@ func extToLangWalkers(walkers map[string]*treesitter.Walker) map[string]*treesit
8791
return result
8892
}
8993

94+
// @intent convert search result nodes into stable eval comparison keys.
9095
func nodeToKeys(nodes []model.Node) []string {
9196
keys := make([]string, len(nodes))
9297
for i, n := range nodes {

internal/eval/metrics.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
// @index Ranking and classification metrics for parser and search evaluation.
12
package eval
23

34
import "math"
45

6+
// ClassificationMetrics holds precision, recall, and F1 counts for one comparison.
7+
// @intent expose set-based scoring fields used by parser node and edge evaluation.
58
type ClassificationMetrics struct {
69
TruePositive int
710
FalsePositive int
@@ -11,6 +14,8 @@ type ClassificationMetrics struct {
1114
F1 float64
1215
}
1316

17+
// ComputeClassification computes set-based precision, recall, and F1 for expected versus actual keys.
18+
// @intent provide one reusable metric primitive for both parser node and edge evaluation.
1419
func ComputeClassification(expected, actual []string) ClassificationMetrics {
1520
expectedSet := make(map[string]bool, len(expected))
1621
for _, e := range expected {
@@ -48,6 +53,7 @@ func ComputeClassification(expected, actual []string) ClassificationMetrics {
4853
return m
4954
}
5055

56+
// @intent measure how many of the top-k ranked results are relevant.
5157
func PrecisionAtK(ranked []string, relevant map[string]bool, k int) float64 {
5258
if k <= 0 || len(ranked) == 0 || len(relevant) == 0 {
5359
return 0
@@ -65,6 +71,7 @@ func PrecisionAtK(ranked []string, relevant map[string]bool, k int) float64 {
6571
return float64(hits) / float64(n)
6672
}
6773

74+
// @intent measure how much of the relevant set appears within the top-k ranked results.
6875
func RecallAtK(ranked []string, relevant map[string]bool, k int) float64 {
6976
if k <= 0 || len(ranked) == 0 || len(relevant) == 0 {
7077
return 0
@@ -82,6 +89,7 @@ func RecallAtK(ranked []string, relevant map[string]bool, k int) float64 {
8289
return float64(hits) / float64(len(relevant))
8390
}
8491

92+
// @intent score the position of the first relevant result for ranking evaluation.
8593
func MRR(ranked []string, relevant map[string]bool) float64 {
8694
for i, r := range ranked {
8795
if relevant[r] {
@@ -91,6 +99,7 @@ func MRR(ranked []string, relevant map[string]bool) float64 {
9199
return 0
92100
}
93101

102+
// @intent compare ranked retrieval quality against an ideal ordering using discounted gain.
94103
func NDCG(ranked []string, relevant map[string]bool, k int) float64 {
95104
if k <= 0 || len(relevant) == 0 {
96105
return 0

internal/eval/parser.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// @index Golden corpus loading and normalization helpers for parser evaluation.
12
package eval
23

34
import (
@@ -9,6 +10,7 @@ import (
910
"github.com/tae2089/code-context-graph/internal/model"
1011
)
1112

13+
// @intent load every language-specific golden corpus file from the eval corpus directory tree.
1214
func LoadGoldenDir(dir string) ([]GoldenCorpus, error) {
1315
var corpora []GoldenCorpus
1416
entries, err := os.ReadDir(dir)
@@ -45,6 +47,7 @@ func LoadGoldenDir(dir string) ([]GoldenCorpus, error) {
4547
return corpora, nil
4648
}
4749

50+
// @intent persist normalized parser output as a golden snapshot for future comparisons.
4851
func WriteGolden(path string, corpus GoldenCorpus) error {
4952
data, err := json.MarshalIndent(corpus, "", " ")
5053
if err != nil {
@@ -53,6 +56,7 @@ func WriteGolden(path string, corpus GoldenCorpus) error {
5356
return os.WriteFile(path, append(data, '\n'), 0o644)
5457
}
5558

59+
// @intent project eval nodes into stable comparison keys for set-based metrics.
5660
func NodeKeys(nodes []EvalNode) []string {
5761
keys := make([]string, len(nodes))
5862
for i, n := range nodes {
@@ -61,6 +65,7 @@ func NodeKeys(nodes []EvalNode) []string {
6165
return keys
6266
}
6367

68+
// @intent project eval edges into stable comparison keys for set-based metrics.
6469
func EdgeKeys(edges []EvalEdge) []string {
6570
keys := make([]string, len(edges))
6671
for i, e := range edges {
@@ -69,6 +74,7 @@ func EdgeKeys(edges []EvalEdge) []string {
6974
return keys
7075
}
7176

77+
// @intent summarize parser accuracy for one language corpus by comparing expected and actual nodes and edges.
7278
func CompareCorpus(expected, actual GoldenCorpus) LanguageReport {
7379
nodeMetrics := ComputeClassification(NodeKeys(expected.Nodes), NodeKeys(actual.Nodes))
7480
edgeMetrics := ComputeClassification(EdgeKeys(expected.Edges), EdgeKeys(actual.Edges))
@@ -80,6 +86,7 @@ func CompareCorpus(expected, actual GoldenCorpus) LanguageReport {
8086
}
8187
}
8288

89+
// @intent normalize parsed graph nodes into corpus-stable eval records independent of absolute paths.
8390
func NormalizeNodes(nodes []model.Node, baseDir string) []EvalNode {
8491
out := make([]EvalNode, 0, len(nodes))
8592
for _, n := range nodes {
@@ -102,6 +109,7 @@ func NormalizeNodes(nodes []model.Node, baseDir string) []EvalNode {
102109
return out
103110
}
104111

112+
// @intent normalize parsed graph edges into corpus-stable eval records keyed by qualified names.
105113
func NormalizeEdges(edges []model.Edge, nodeMap map[uint]string) []EvalEdge {
106114
out := make([]EvalEdge, 0, len(edges))
107115
for _, e := range edges {

internal/eval/runner.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// @index Eval runner orchestration for parser and search benchmark suites.
12
package eval
23

34
import (
@@ -13,6 +14,8 @@ import (
1314
"github.com/tae2089/code-context-graph/internal/parse/treesitter"
1415
)
1516

17+
// RunOptions collects corpus paths, parser walkers, and output settings for one eval invocation.
18+
// @intent configure parser/search evaluation suites from CLI without leaking command details.
1619
type RunOptions struct {
1720
CorpusDir string
1821
Suite string
@@ -23,6 +26,8 @@ type RunOptions struct {
2326
Writer io.Writer
2427
}
2528

29+
// Run executes the requested evaluation suites and writes the chosen report format.
30+
// @intent provide one orchestration entry point for CLI-driven parser and search evaluation.
2631
func Run(ctx context.Context, opts RunOptions) (*Report, error) {
2732
report := &Report{Suite: opts.Suite}
2833

@@ -44,13 +49,15 @@ func Run(ctx context.Context, opts RunOptions) (*Report, error) {
4449
return report, writeTable(opts.Writer, report)
4550
}
4651

52+
// @intent route parser evaluation into update or comparison mode without duplicating top-level flow control.
4753
func runParserEval(ctx context.Context, opts RunOptions, report *Report) error {
4854
if opts.Update {
4955
return runParserUpdate(ctx, opts)
5056
}
5157
return runParserCompare(ctx, opts, report)
5258
}
5359

60+
// @intent regenerate golden parser snapshots from the current parser implementation.
5461
func runParserUpdate(ctx context.Context, opts RunOptions) error {
5562
entries, err := os.ReadDir(opts.CorpusDir)
5663
if err != nil {
@@ -102,6 +109,7 @@ func runParserUpdate(ctx context.Context, opts RunOptions) error {
102109
return nil
103110
}
104111

112+
// @intent compare current parser output against stored golden corpora and accumulate language reports.
105113
func runParserCompare(ctx context.Context, opts RunOptions, report *Report) error {
106114
corpora, err := LoadGoldenDir(opts.CorpusDir)
107115
if err != nil {
@@ -158,6 +166,7 @@ func runParserCompare(ctx context.Context, opts RunOptions, report *Report) erro
158166
return nil
159167
}
160168

169+
// @intent run ranking metrics over the search corpus when a search backend is available.
161170
func runSearchEval(_ context.Context, opts RunOptions, report *Report) error {
162171
queryPath := fmt.Sprintf("%s/queries.json", opts.CorpusDir)
163172
qc, err := LoadQueryCorpus(queryPath)
@@ -177,6 +186,7 @@ func runSearchEval(_ context.Context, opts RunOptions, report *Report) error {
177186
return nil
178187
}
179188

189+
// @intent map parsed node IDs to qualified names so edge normalization can avoid DB lookups.
180190
func buildNodeMap(nodes []model.Node) map[uint]string {
181191
m := make(map[uint]string, len(nodes))
182192
for _, n := range nodes {
@@ -185,16 +195,19 @@ func buildNodeMap(nodes []model.Node) map[uint]string {
185195
return m
186196
}
187197

198+
// @intent centralize file reads used by parser evaluation so tests can characterize failures consistently.
188199
func readFileContent(path string) ([]byte, error) {
189200
return os.ReadFile(path)
190201
}
191202

203+
// @intent emit the eval report as pretty-printed JSON for machine consumption.
192204
func writeJSON(w io.Writer, report *Report) error {
193205
enc := json.NewEncoder(w)
194206
enc.SetIndent("", " ")
195207
return enc.Encode(report)
196208
}
197209

210+
// @intent render a compact human-readable summary of parser and search evaluation metrics.
198211
func writeTable(w io.Writer, report *Report) error {
199212
if len(report.Languages) > 0 {
200213
fmt.Fprintf(w, "=== Parser Evaluation ===\n\n")

internal/eval/schema.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
// @index Data schemas for parser golden corpora and search evaluation reports.
12
package eval
23

4+
// EvalNode is the normalized parser node shape used in golden corpora.
5+
// @intent capture corpus-stable node identity independent of absolute paths.
36
type EvalNode struct {
47
ID string `json:"id"`
58
Kind string `json:"kind"`
@@ -9,45 +12,61 @@ type EvalNode struct {
912
EndLine int `json:"end_line,omitempty"`
1013
}
1114

15+
// Key derives a stable string key for node-level evaluation comparisons.
16+
// @intent provide a deterministic identifier for set-based parser metrics.
1217
func (n EvalNode) Key() string {
1318
return n.Kind + ":" + n.Name + "@" + n.File
1419
}
1520

21+
// EvalEdge is the normalized parser edge shape used in golden corpora.
22+
// @intent capture corpus-stable edge identity for parser comparisons.
1623
type EvalEdge struct {
1724
Kind string `json:"kind"`
1825
From string `json:"from"`
1926
To string `json:"to"`
2027
}
2128

29+
// Key derives a stable string key for edge-level evaluation comparisons.
30+
// @intent provide a deterministic identifier for set-based parser metrics.
2231
func (e EvalEdge) Key() string {
2332
return e.Kind + ":" + e.From + "->" + e.To
2433
}
2534

35+
// GoldenCorpus stores one parser snapshot for a source file in a language corpus.
36+
// @intent persist expected parser output for regression comparison.
2637
type GoldenCorpus struct {
2738
Language string `json:"language"`
2839
File string `json:"file"`
2940
Nodes []EvalNode `json:"nodes"`
3041
Edges []EvalEdge `json:"edges"`
3142
}
3243

44+
// QueryCase defines one search evaluation query and its relevant expected results.
45+
// @intent describe a single ranked-retrieval test case for search evaluation.
3346
type QueryCase struct {
3447
Query string `json:"query"`
3548
Relevant []string `json:"relevant"`
3649
K int `json:"k,omitempty"`
3750
}
3851

52+
// QueryCorpus groups search evaluation cases loaded from one corpus directory.
53+
// @intent represent the full search evaluation suite as one loadable artifact.
3954
type QueryCorpus struct {
4055
CorpusDir string `json:"corpus_dir"`
4156
Queries []QueryCase `json:"queries"`
4257
}
4358

59+
// LanguageReport summarizes parser accuracy metrics for one language corpus.
60+
// @intent expose per-language node and edge metrics in the eval report.
4461
type LanguageReport struct {
4562
Language string `json:"language"`
4663
NodeMetrics ClassificationMetrics `json:"node_metrics"`
4764
EdgeMetrics ClassificationMetrics `json:"edge_metrics"`
4865
Files int `json:"files"`
4966
}
5067

68+
// SearchReport holds aggregate ranking metrics across the search evaluation corpus.
69+
// @intent surface average P@K, recall, MRR, and nDCG over all search queries.
5170
type SearchReport struct {
5271
QueriesTotal int `json:"queries_total"`
5372
AvgPAt1 float64 `json:"avg_p_at_1"`
@@ -58,6 +77,8 @@ type SearchReport struct {
5877
AvgNDCGAt5 float64 `json:"avg_ndcg_at_5"`
5978
}
6079

80+
// Report bundles parser and search evaluation output into one CLI-facing payload.
81+
// @intent represent the complete eval result returned to CLI and JSON consumers.
6182
type Report struct {
6283
Suite string `json:"suite"`
6384
Languages []LanguageReport `json:"languages,omitempty"`

internal/eval/search.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
// @index Search evaluation corpus loading and ranking metric orchestration.
12
package eval
23

34
import (
45
"encoding/json"
56
"os"
67
)
78

9+
// SearchFunc abstracts search execution so evaluation can run against any backend.
10+
// @intent decouple ranking metrics from concrete search implementations.
811
type SearchFunc func(query string, limit int) ([]string, error)
912

13+
// LoadQueryCorpus loads search evaluation cases from a JSON corpus file.
14+
// @intent ingest the search query corpus before running ranking evaluation.
1015
func LoadQueryCorpus(path string) (QueryCorpus, error) {
1116
data, err := os.ReadFile(path)
1217
if err != nil {
@@ -19,6 +24,7 @@ func LoadQueryCorpus(path string) (QueryCorpus, error) {
1924
return qc, nil
2025
}
2126

27+
// @intent execute ranked retrieval metrics across all search evaluation cases.
2228
func EvaluateQueries(cases []QueryCase, searchFn SearchFunc) (SearchReport, error) {
2329
if len(cases) == 0 {
2430
return SearchReport{}, nil

0 commit comments

Comments
 (0)