Skip to content

Commit a2e84f8

Browse files
docs(benchmark): annotate benchmark corpus and runners
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 292ddfa commit a2e84f8

9 files changed

Lines changed: 84 additions & 0 deletions

File tree

internal/benchmark/analyze.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
// @index Match analysis helpers for benchmark run results.
12
package benchmark
23

34
import "strings"
45

56
// fileMatches reports whether actual matches expected, supporting both
67
// exact match and suffix match to handle absolute vs relative path differences.
78
// e.g. expected "internal/webhook/handler.go" matches actual "/home/user/repo/internal/webhook/handler.go"
9+
// @intent compare expected files against recorded reads without depending on absolute path stability.
810
func fileMatches(expected, actual string) bool {
911
if expected == actual {
1012
return true
@@ -15,6 +17,7 @@ func fileMatches(expected, actual string) bool {
1517
// MatchFiles computes the ratio of expected files found in FilesRead or mentioned
1618
// in the Answer text (as a fallback when tool-call data is unavailable).
1719
// Returns 1.0 if no expected files are specified.
20+
// @intent score whether a benchmark run inspected the files the query was expected to touch.
1821
func MatchFiles(result RunResult, query Query) float64 {
1922
if len(query.ExpectedFiles) == 0 {
2023
return 1.0
@@ -41,6 +44,7 @@ func MatchFiles(result RunResult, query Query) float64 {
4144

4245
// MatchSymbols computes the ratio of expected symbols found in the answer text.
4346
// Returns 1.0 if no expected symbols are specified.
47+
// @intent score whether the final answer mentioned the symbols the query was expected to surface.
4448
func MatchSymbols(result RunResult, query Query) float64 {
4549
if len(query.ExpectedSymbols) == 0 {
4650
return 1.0
@@ -55,6 +59,7 @@ func MatchSymbols(result RunResult, query Query) float64 {
5559
}
5660

5761
// CountCcgToolCalls returns the number of tool calls using mcp__ccg__ prefix.
62+
// @intent quantify how much a benchmark run relied on CCG-specific MCP tools.
5863
func CountCcgToolCalls(result RunResult) int {
5964
var count int
6065
for _, tc := range result.ToolCalls {
@@ -66,6 +71,7 @@ func CountCcgToolCalls(result RunResult) int {
6671
}
6772

6873
// ComputeMatch derives a MatchResult from a single RunResult and its Query.
74+
// @intent consolidate per-query benchmark scoring into one reusable result structure.
6975
func ComputeMatch(result RunResult, query Query) MatchResult {
7076
return MatchResult{
7177
QueryID: result.QueryID,
@@ -79,6 +85,7 @@ func ComputeMatch(result RunResult, query Query) MatchResult {
7985

8086
// AnalyzeRun computes MatchResult for every result in the run against the corpus.
8187
// Results with no matching query are skipped.
88+
// @intent turn a full benchmark run into per-query scored matches against the corpus definition.
8289
func AnalyzeRun(run *BenchmarkRun, corpus *Corpus) []MatchResult {
8390
queryMap := make(map[string]Query, len(corpus.Queries))
8491
for _, q := range corpus.Queries {

internal/benchmark/compare.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
// @index Comparison helpers for benchmark runs with and without CCG.
12
package benchmark
23

34
// Compare builds a ComparisonReport from a with-ccg run and an optional without-ccg run.
45
// Matches contains with-ccg metrics; MatchesWithout contains without-ccg metrics when provided.
6+
// @intent produce one report that captures both benchmark modes and their scored query matches.
57
func Compare(withCCG *BenchmarkRun, withoutCCG *BenchmarkRun, corpus *Corpus) *ComparisonReport {
68
report := &ComparisonReport{
79
WithCCG: withCCG,

internal/benchmark/corpus.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// @index Benchmark corpus loading, saving, and validation utilities.
12
package benchmark
23

34
import (
@@ -12,6 +13,7 @@ import (
1213
var validQueryID = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`)
1314

1415
// LoadCorpus reads a queries.yaml file and validates its contents.
16+
// @intent load a benchmark corpus from disk while enforcing schema and ID constraints up front.
1517
func LoadCorpus(path string) (*Corpus, error) {
1618
data, err := os.ReadFile(path)
1719
if err != nil {
@@ -28,6 +30,7 @@ func LoadCorpus(path string) (*Corpus, error) {
2830
}
2931

3032
// SaveCorpus writes a Corpus to a YAML file.
33+
// @intent persist benchmark corpus definitions in the YAML format used by the CLI.
3134
func SaveCorpus(path string, c *Corpus) error {
3235
data, err := yaml.Marshal(c)
3336
if err != nil {
@@ -40,6 +43,7 @@ func SaveCorpus(path string, c *Corpus) error {
4043
}
4144

4245
// ValidateCorpus checks that all queries have required fields and no duplicate IDs.
46+
// @intent reject malformed benchmark corpora before any benchmark execution depends on them.
4347
func ValidateCorpus(c *Corpus) error {
4448
if len(c.Queries) == 0 {
4549
return fmt.Errorf("corpus has no queries")

internal/benchmark/jsonl.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// @index JSONL session parsing and extraction helpers for benchmark analysis.
12
package benchmark
23

34
import (
@@ -15,6 +16,7 @@ var (
1516
)
1617

1718
// SessionMessage represents one line from a Claude Code session JSONL file.
19+
// @intent model the subset of Claude session JSONL needed to reconstruct benchmark runs.
1820
type SessionMessage struct {
1921
Type string `json:"type"`
2022
Message *MessagePayload `json:"message,omitempty"`
@@ -23,13 +25,15 @@ type SessionMessage struct {
2325
}
2426

2527
// MessagePayload is the nested message object inside a SessionMessage.
28+
// @intent expose assistant role, content blocks, and usage data from session lines.
2629
type MessagePayload struct {
2730
Role string `json:"role"`
2831
Content []ContentBlock `json:"content"`
2932
Usage *UsageInfo `json:"usage,omitempty"`
3033
}
3134

3235
// ContentBlock is a single content item (text, tool_use, etc.).
36+
// @intent represent individual Claude message blocks so benchmark analyzers can inspect tool calls and text.
3337
type ContentBlock struct {
3438
Type string `json:"type"`
3539
Text string `json:"text,omitempty"`
@@ -39,19 +43,22 @@ type ContentBlock struct {
3943
}
4044

4145
// UsageInfo holds token usage from Claude's response.
46+
// @intent capture token accounting for one Claude message so benchmark runs can aggregate usage.
4247
type UsageInfo struct {
4348
InputTokens int `json:"input_tokens"`
4449
OutputTokens int `json:"output_tokens"`
4550
}
4651

4752
// QuerySegment groups the messages belonging to a single benchmark query.
53+
// @intent isolate the portion of a session that belongs to one marked benchmark query.
4854
type QuerySegment struct {
4955
QueryID string
5056
Messages []SessionMessage
5157
}
5258

5359
// ParseJSONL reads a Claude Code session JSONL file and returns all parsed lines.
5460
// Lines that are not valid JSON are silently skipped.
61+
// @intent ingest recorded Claude sessions even when they contain partial or non-JSON noise lines.
5562
func ParseJSONL(path string) ([]SessionMessage, error) {
5663
f, err := os.Open(path)
5764
if err != nil {
@@ -83,6 +90,7 @@ const (
8390

8491
// extractMarker checks if a SessionMessage contains a benchmark marker.
8592
// Returns (queryID, isStart, found). Uses strict regex to prevent injection via crafted IDs.
93+
// @intent locate benchmark query boundaries inside session transcripts safely.
8694
func extractMarker(m SessionMessage) (queryID string, isStart bool, found bool) {
8795
var text string
8896
if m.Type == "tool_result" {
@@ -107,6 +115,7 @@ func extractMarker(m SessionMessage) (queryID string, isStart bool, found bool)
107115

108116
// ExtractQuerySegments splits a session's messages into per-query segments using markers.
109117
// An unclosed segment (START without END) is included, spanning to the last message.
118+
// @intent reconstruct per-query transcript slices from a whole benchmark session log.
110119
func ExtractQuerySegments(msgs []SessionMessage) ([]QuerySegment, error) {
111120
var segs []QuerySegment
112121
var current *QuerySegment
@@ -134,6 +143,7 @@ func ExtractQuerySegments(msgs []SessionMessage) ([]QuerySegment, error) {
134143
}
135144

136145
// ExtractToolCalls collects all tool_use blocks from a segment's messages.
146+
// @intent recover structured tool invocation history for one benchmark query.
137147
func ExtractToolCalls(seg QuerySegment) []ToolCall {
138148
var calls []ToolCall
139149
for _, m := range seg.Messages {
@@ -154,6 +164,7 @@ func ExtractToolCalls(seg QuerySegment) []ToolCall {
154164
}
155165

156166
// ExtractFilesRead returns deduplicated file paths from Read tool calls in the segment.
167+
// @intent estimate file-inspection behavior from tool-use records without double-counting reads.
157168
func ExtractFilesRead(seg QuerySegment) []string {
158169
var files []string
159170
seen := make(map[string]bool)
@@ -178,6 +189,7 @@ func ExtractFilesRead(seg QuerySegment) []string {
178189
}
179190

180191
// ExtractTokens sums input and output token counts across all messages in a segment.
192+
// @intent aggregate token usage for the full lifecycle of one benchmark query.
181193
func ExtractTokens(seg QuerySegment) (inputTokens, outputTokens int) {
182194
for _, m := range seg.Messages {
183195
if m.Message != nil && m.Message.Usage != nil {
@@ -189,6 +201,7 @@ func ExtractTokens(seg QuerySegment) (inputTokens, outputTokens int) {
189201
}
190202

191203
// ExtractAnswer returns the text of the last assistant text block in the segment.
204+
// @intent capture the final assistant answer that should be evaluated for symbol hits.
192205
func ExtractAnswer(seg QuerySegment) string {
193206
var last string
194207
for _, m := range seg.Messages {
@@ -205,6 +218,7 @@ func ExtractAnswer(seg QuerySegment) string {
205218
}
206219

207220
// ExtractRunResult builds a RunResult from a query segment.
221+
// @intent convert an extracted query segment into the canonical benchmark run result shape.
208222
func ExtractRunResult(queryID string, seg QuerySegment) RunResult {
209223
in, out := ExtractTokens(seg)
210224
return RunResult{

internal/benchmark/report.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// @index Markdown report generation for benchmark comparisons.
12
package benchmark
23

34
import (
@@ -7,6 +8,7 @@ import (
78
)
89

910
// WriteReport renders a ComparisonReport as markdown and writes it to outPath.
11+
// @intent turn benchmark comparison data into a readable artifact for sharing and regression tracking.
1012
func WriteReport(report *ComparisonReport, outPath string) error {
1113
if report.WithCCG == nil {
1214
return fmt.Errorf("comparison report missing with-ccg run")

internal/benchmark/runner.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// @index Benchmark runner orchestration for Claude CLI subprocess execution.
12
package benchmark
23

34
import (
@@ -11,11 +12,13 @@ import (
1112

1213
// Executor abstracts the subprocess execution so tests can inject a mock.
1314
// dir sets the working directory for the subprocess; empty means inherit from parent.
15+
// @intent decouple benchmark orchestration from the concrete Claude CLI process implementation.
1416
type Executor interface {
1517
Execute(ctx context.Context, args []string, prompt, dir string) ([]byte, error)
1618
}
1719

1820
// RunnerConfig holds configuration for a benchmark run.
21+
// @intent collect execution mode, workspace, and timeout settings for one benchmark run.
1922
type RunnerConfig struct {
2023
Mode string // "with-ccg" | "without-ccg"
2124
CWD string // benchmark workspace directory
@@ -24,6 +27,7 @@ type RunnerConfig struct {
2427
}
2528

2629
// DefaultRunnerConfig returns a RunnerConfig with sensible defaults.
30+
// @intent provide conservative benchmark defaults that work for most local runs.
2731
func DefaultRunnerConfig() RunnerConfig {
2832
return RunnerConfig{
2933
MaxToolCalls: 50,
@@ -32,6 +36,7 @@ func DefaultRunnerConfig() RunnerConfig {
3236
}
3337

3438
// claudeOutput is the structure of `claude -p --output-format json` output.
39+
// @intent decode the subset of Claude CLI JSON output needed for benchmark scoring.
3540
type claudeOutput struct {
3641
IsError bool `json:"is_error"`
3742
Result string `json:"result"`
@@ -43,6 +48,7 @@ type claudeOutput struct {
4348

4449
// buildArgs constructs CLI args for a specific run.
4550
// mcpConfigFile is the path to an empty MCP config file for without-ccg mode.
51+
// @intent centralize Claude CLI argument construction for both benchmark modes.
4652
func buildArgs(cfg RunnerConfig, mcpConfigFile string) []string {
4753
args := []string{"-p", "--output-format", "json"}
4854
if cfg.MaxToolCalls > 0 {
@@ -58,6 +64,7 @@ func buildArgs(cfg RunnerConfig, mcpConfigFile string) []string {
5864
// Working directory is set on the subprocess directly (not via --cwd flag).
5965
// In "without-ccg" mode, --strict-mcp-config disables all MCP servers.
6066
// NOTE: For actual runs, use Runner.Run which creates a real temp file for --mcp-config.
67+
// @intent expose deterministic argument construction for tests and tooling that inspect benchmark invocation behavior.
6168
func BuildClaudeArgs(cfg RunnerConfig) []string {
6269
if cfg.Mode == "without-ccg" {
6370
return buildArgs(cfg, `{"mcpServers":{}}`)
@@ -67,6 +74,7 @@ func BuildClaudeArgs(cfg RunnerConfig) []string {
6774

6875
// BuildPrompt creates the prompt string that wraps a query with benchmark markers.
6976
// The markers allow the JSONL analyzer to locate query boundaries.
77+
// @intent surround benchmark queries with markers so later session analysis can recover exact query segments.
7078
func BuildPrompt(q Query) string {
7179
return fmt.Sprintf(
7280
"===BENCHMARK_QUERY_START id=%s===\n%s\n===BENCHMARK_QUERY_END id=%s===",
@@ -75,18 +83,21 @@ func BuildPrompt(q Query) string {
7583
}
7684

7785
// Runner executes benchmark queries sequentially using the provided Executor.
86+
// @intent own the end-to-end lifecycle of benchmark query execution for one mode.
7887
type Runner struct {
7988
cfg RunnerConfig
8089
exec Executor
8190
}
8291

8392
// NewRunner creates a Runner with the given config and executor.
93+
// @intent assemble benchmark orchestration from execution config and a subprocess adapter.
8494
func NewRunner(cfg RunnerConfig, exec Executor) *Runner {
8595
return &Runner{cfg: cfg, exec: exec}
8696
}
8797

8898
// Run executes each query in the corpus and returns a BenchmarkRun.
8999
// Executor errors are captured in RunResult.Error rather than aborting the run.
100+
// @intent execute the full benchmark corpus while preserving per-query failures in the final report.
90101
func (r *Runner) Run(ctx context.Context, corpus *Corpus) (*BenchmarkRun, error) {
91102
run := &BenchmarkRun{
92103
Mode: r.cfg.Mode,
@@ -136,6 +147,7 @@ func (r *Runner) Run(ctx context.Context, corpus *Corpus) (*BenchmarkRun, error)
136147
// prepareMCPConfig creates a temp JSON file for without-ccg mode so that
137148
// --mcp-config receives a file path rather than inline JSON (avoiding the
138149
// variadic flag consuming the prompt from stdin).
150+
// @intent disable MCP cleanly in without-ccg mode without confusing the Claude CLI argument parser.
139151
func (r *Runner) prepareMCPConfig() (path string, cleanup func(), err error) {
140152
if r.cfg.Mode != "without-ccg" {
141153
return "", func() {}, nil
@@ -154,6 +166,7 @@ func (r *Runner) prepareMCPConfig() (path string, cleanup func(), err error) {
154166
}
155167

156168
// parseClaudeOutput extracts token counts and answer text from claude's JSON output.
169+
// @intent translate Claude CLI JSON output into benchmark result fields without failing the whole run on parse misses.
157170
func (r *Runner) parseClaudeOutput(out []byte, res *RunResult) {
158171
if len(out) == 0 {
159172
return

internal/benchmark/schema.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package benchmark
44
import "time"
55

66
// Query represents a single benchmark query with expected results.
7+
// @intent define one benchmark prompt and the files or symbols it is expected to surface.
78
type Query struct {
89
ID string `yaml:"id" json:"id"`
910
Description string `yaml:"description" json:"description"`
@@ -13,18 +14,21 @@ type Query struct {
1314
}
1415

1516
// Corpus holds the collection of benchmark queries.
17+
// @intent group benchmark queries into a reusable corpus that can be run and validated together.
1618
type Corpus struct {
1719
Version string `yaml:"version" json:"version,omitempty"`
1820
Queries []Query `yaml:"queries" json:"queries"`
1921
}
2022

2123
// ToolCall records a single tool invocation during query execution.
24+
// @intent capture the tool usage footprint of one benchmarked query execution.
2225
type ToolCall struct {
2326
Tool string `json:"tool"`
2427
Input string `json:"input,omitempty"`
2528
}
2629

2730
// RunResult captures the outcome of executing a single query.
31+
// @intent store answer text, tool usage, timing, and token counts for one benchmark query.
2832
type RunResult struct {
2933
QueryID string `json:"query_id"`
3034
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
@@ -37,13 +41,15 @@ type RunResult struct {
3741
}
3842

3943
// BenchmarkRun holds all results from a single benchmark execution.
44+
// @intent represent one complete benchmark session for a given execution mode.
4045
type BenchmarkRun struct {
4146
Mode string `json:"mode"`
4247
RunAt time.Time `json:"run_at"`
4348
Results []RunResult `json:"results"`
4449
}
4550

4651
// ResultByID returns the RunResult for the given query ID, or nil if not found.
52+
// @intent support direct lookup of a query result inside a benchmark run.
4753
func (r *BenchmarkRun) ResultByID(id string) *RunResult {
4854
for i := range r.Results {
4955
if r.Results[i].QueryID == id {
@@ -54,6 +60,7 @@ func (r *BenchmarkRun) ResultByID(id string) *RunResult {
5460
}
5561

5662
// MatchResult holds the computed match metrics for a single query.
63+
// @intent summarize scored file, symbol, tool, and token metrics for one benchmark query.
5764
type MatchResult struct {
5865
QueryID string `json:"query_id"`
5966
FileHitRatio float64 `json:"file_hit_ratio"`
@@ -64,6 +71,7 @@ type MatchResult struct {
6471
}
6572

6673
// ComparisonReport holds a comparison between two benchmark runs.
74+
// @intent package benchmark runs and their scored matches into one comparison artifact.
6775
type ComparisonReport struct {
6876
WithCCG *BenchmarkRun `json:"with_ccg"`
6977
WithoutCCG *BenchmarkRun `json:"without_ccg,omitempty"`

0 commit comments

Comments
 (0)