Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,101 @@
},
"code_changes": {
"type": "object",
"description": "Summary of code modifications"
"description": "Summary of code modifications",
"properties": {
"files_modified": {
"type": "integer",
"description": "Number of files modified"
},
"files_created": {
"type": "integer",
"description": "Number of files created"
},
"files_deleted": {
"type": "integer",
"description": "Number of files deleted"
},
"lines_added": {
"type": "integer",
"description": "Total lines added"
},
"lines_removed": {
"type": "integer",
"description": "Total lines removed"
},
"ai_lines_added": {
"type": "integer",
"description": "Lines added attributed to AI"
},
"ai_lines_removed": {
"type": "integer",
"description": "Lines removed attributed to AI"
},
"human_lines_added": {
"type": "integer",
"description": "Lines added attributed to human"
},
"human_lines_removed": {
"type": "integer",
"description": "Lines removed attributed to human"
},
"files": {
"type": "array",
"description": "Per-file change details",
"items": {
"type": "object",
"required": ["path", "status"],
"properties": {
"path": {
"type": "string",
"description": "File path"
},
"status": {
"type": "string",
"description": "Change status (e.g. modified, created, deleted)"
},
"lines_added": {
"type": "integer",
"description": "Lines added in this file"
},
"lines_removed": {
"type": "integer",
"description": "Lines removed in this file"
},
"attribution": {
"type": "string",
"description": "Change attribution: ai or human"
},
"line_ranges": {
"type": "array",
"description": "Contiguous line ranges",
"items": {
"type": "object",
"required": ["start", "end"],
"properties": {
"start": {
"type": "integer",
"description": "Start line number"
},
"end": {
"type": "integer",
"description": "End line number"
}
},
"additionalProperties": false
}
},
"session_ids": {
"type": "array",
"description": "Session IDs associated with changes in this file",
"items": {
"type": "string"
}
}
}
}
}
}
},
"model": {
"type": "object",
Expand All @@ -89,9 +183,37 @@
},
"subagents": {
"type": "array",
"description": "Subagent information (open schema)",
"description": "Subagent information",
"items": {
"type": "object"
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique subagent identifier"
},
"type": {
"type": "string",
"description": "Subagent type"
},
"description": {
"type": "string",
"description": "Subagent description"
},
"tokens": {
"type": "object",
"description": "Token usage for this subagent",
"properties": {
"input": {
"type": "integer",
"description": "Input tokens consumed"
},
"output": {
"type": "integer",
"description": "Output tokens produced"
}
}
}
}
}
},
"raw_session": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,35 @@ type GitContext struct {
CommitCount int `json:"commit_count,omitempty"`
}

// LineRange represents a contiguous range of lines.
type LineRange struct {
Start int `json:"start"`
End int `json:"end"`
}

// FileChange represents a single file modification in the session.
type FileChange struct {
Path string `json:"path"`
Status string `json:"status"`
Path string `json:"path"`
Status string `json:"status"`
LinesAdded int `json:"lines_added,omitempty"`
LinesRemoved int `json:"lines_removed,omitempty"`
Attribution string `json:"attribution,omitempty"`
LineRanges []LineRange `json:"line_ranges,omitempty"`
SessionIDs []string `json:"session_ids,omitempty"`
}

// CodeChanges summarizes code modifications made during the session.
type CodeChanges struct {
FilesModified int `json:"files_modified,omitempty"`
FilesCreated int `json:"files_created,omitempty"`
FilesDeleted int `json:"files_deleted,omitempty"`
LinesAdded int `json:"lines_added,omitempty"`
LinesRemoved int `json:"lines_removed,omitempty"`
Files []FileChange `json:"files,omitempty"`
FilesModified int `json:"files_modified,omitempty"`
FilesCreated int `json:"files_created,omitempty"`
FilesDeleted int `json:"files_deleted,omitempty"`
LinesAdded int `json:"lines_added,omitempty"`
LinesRemoved int `json:"lines_removed,omitempty"`
AILinesAdded int `json:"ai_lines_added,omitempty"`
AILinesRemoved int `json:"ai_lines_removed,omitempty"`
HumanLinesAdded int `json:"human_lines_added,omitempty"`
HumanLinesRemoved int `json:"human_lines_removed,omitempty"`
Files []FileChange `json:"files,omitempty"`
}

// Model holds information about the AI models used in the session.
Expand Down Expand Up @@ -95,6 +110,20 @@ type ToolsUsed struct {
TotalInvocations int `json:"total_invocations,omitempty"`
}

// SubagentTokens holds token usage for a subagent.
type SubagentTokens struct {
Input int `json:"input"`
Output int `json:"output"`
}

// Subagent describes a spawned subagent within the session.
type Subagent struct {
ID string `json:"id"`
Type string `json:"type"`
Description string `json:"description"`
Tokens SubagentTokens `json:"tokens"`
}

// Conversation holds message count statistics.
type Conversation struct {
TotalMessages int `json:"total_messages,omitempty"`
Expand All @@ -113,7 +142,7 @@ type Data struct {
Usage *Usage `json:"usage,omitempty"`
ToolsUsed *ToolsUsed `json:"tools_used,omitempty"`
Conversation *Conversation `json:"conversation,omitempty"`
Subagents []json.RawMessage `json:"subagents,omitempty"`
Subagents []Subagent `json:"subagents,omitempty"`
RawSession map[string][]json.RawMessage `json:"raw_session,omitempty"`
Warnings []string `json:"warnings,omitempty"`
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,68 @@ func TestChainloopAICodingSessionCrafter_RejectsExtraFields(t *testing.T) {
assert.Contains(t, err.Error(), "AI coding session validation failed")
}

func TestChainloopAICodingSessionCrafter_RealWorldEvidence(t *testing.T) {
// Load real-world evidence that uses the new attribution, line range, and subagent fields.
raw, err := os.ReadFile("./testdata/ai-coding-session-with-attribution.json")
require.NoError(t, err)

var evidence aicodingsession.Evidence
require.NoError(t, json.Unmarshal(raw, &evidence))

data := evidence.Data

// Top-level identifiers
assert.Equal(t, aicodingsession.EvidenceID, evidence.ID)
assert.Equal(t, aicodingsession.EvidenceSchemaURL, evidence.Schema)
assert.Equal(t, "v1", data.SchemaVersion)

// Agent
assert.Equal(t, "claude-code", data.Agent.Name)
assert.Equal(t, "2.1.92", data.Agent.Version)

// Session
assert.Equal(t, "1dea3f43-4c8f-4625-8732-349613261162", data.Session.ID)
assert.Equal(t, 14, data.Session.DurationSeconds)

// Code changes — AI/human attribution
require.NotNil(t, data.CodeChanges)
assert.Equal(t, 11, data.CodeChanges.LinesAdded)
assert.Equal(t, 8, data.CodeChanges.AILinesAdded)
assert.Equal(t, 0, data.CodeChanges.AILinesRemoved)
assert.Equal(t, 3, data.CodeChanges.HumanLinesAdded)
assert.Equal(t, 0, data.CodeChanges.HumanLinesRemoved)

// File-level fields
require.Len(t, data.CodeChanges.Files, 2)

goMod := data.CodeChanges.Files[0]
assert.Equal(t, "go.mod", goMod.Path)
assert.Equal(t, "human", goMod.Attribution)
assert.Equal(t, 3, goMod.LinesAdded)

mainGo := data.CodeChanges.Files[1]
assert.Equal(t, "main.go", mainGo.Path)
assert.Equal(t, "ai", mainGo.Attribution)
assert.Equal(t, 8, mainGo.LinesAdded)
require.Len(t, mainGo.LineRanges, 1)
assert.Equal(t, 6, mainGo.LineRanges[0].Start)
assert.Equal(t, 6, mainGo.LineRanges[0].End)
require.Len(t, mainGo.SessionIDs, 1)
assert.Equal(t, "1dea3f43-4c8f-4625-8732-349613261162", mainGo.SessionIDs[0])

// Usage
require.NotNil(t, data.Usage)
assert.InDelta(t, 0.1917, data.Usage.EstimatedCostUSD, 0.0001)

// Schema validation should pass
dataBytes, err := json.Marshal(data)
require.NoError(t, err)

var rawData any
require.NoError(t, json.Unmarshal(dataBytes, &rawData))
require.NoError(t, schemavalidators.ValidateAICodingSession(rawData, schemavalidators.AICodingSessionVersion0_1))
}

func TestChainloopAICodingSessionCrafter_Annotations(t *testing.T) {
testCases := []struct {
name string
Expand All @@ -241,6 +303,13 @@ func TestChainloopAICodingSessionCrafter_Annotations(t *testing.T) {
expectedModel: "claude-opus-4-6",
modelPresent: true,
},
{
name: "session with AI attribution fields",
filePath: "./testdata/ai-coding-session-with-attribution.json",
expectedAgentName: "claude-code",
expectedModel: "claude-opus-4-6",
modelPresent: true,
},
{
name: "minimal session without model",
filePath: "./testdata/ai-coding-session-minimal.json",
Expand Down
Loading
Loading