| 192 | } |
| 193 | |
| 194 | func BuildBlameFile(db *sql.DB, runID, filePath string) (FileBlameManifest, error) { |
| 195 | filePath, err := cleanRelativeFile(filePath) |
| 196 | if err != nil { |
| 197 | return FileBlameManifest{}, err |
| 198 | } |
| 199 | baseSnapshotID, baseRoot, err := baseSnapshotForRun(db, runID) |
| 200 | if err != nil { |
| 201 | return FileBlameManifest{}, err |
| 202 | } |
| 203 | baseContent, baseOK, err := readRelativeFile(baseRoot, filePath) |
| 204 | if err != nil { |
| 205 | return FileBlameManifest{}, err |
| 206 | } |
| 207 | baseHash := hashOrMissing(baseContent, baseOK) |
| 208 | attempts, err := attemptsForRun(db, runID) |
| 209 | if err != nil { |
| 210 | return FileBlameManifest{}, err |
| 211 | } |
| 212 | manifest := FileBlameManifest{SchemaVersion: "agentprovenance.blame/v1", RunID: runID, File: filePath, BaseSnapshotID: baseSnapshotID, BaseSHA256: baseHash} |
| 213 | for _, attempt := range attempts { |
| 214 | content, ok, err := readRelativeFile(attempt.WorkspacePath, filePath) |
| 215 | if err != nil { |
| 216 | return FileBlameManifest{}, err |
| 217 | } |
| 218 | fileHash := "missing" |
| 219 | if ok { |
| 220 | fileHash = sha256Hex(content) |
| 221 | } |
| 222 | changed := fileHash != baseHash |
| 223 | reason := "unchanged_from_base" |
| 224 | if !baseOK && ok { |
| 225 | reason = "created_by_attempt" |
| 226 | } else if baseOK && !ok { |
| 227 | reason = "deleted_by_attempt" |
| 228 | } else if changed { |
| 229 | reason = "modified_by_attempt" |
| 230 | } |
| 231 | manifest.Entries = append(manifest.Entries, FileBlameEntry{ |
| 232 | File: filePath, |
| 233 | AttemptID: attempt.AttemptID, |
| 234 | RolloutID: attempt.RolloutID, |
| 235 | ToolCallID: attempt.ToolCallID, |
| 236 | SnapshotID: attempt.SnapshotID, |
| 237 | IsWinner: attempt.IsWinner, |
| 238 | Changed: changed, |
| 239 | Reason: reason, |
| 240 | SHA256: fileHash, |
| 241 | Status: attempt.Status, |
| 242 | Strategy: attempt.Strategy, |
| 243 | Command: attempt.Command, |
| 244 | ArtifactRef: attempt.ArtifactRef, |
| 245 | WorkspacePath: attempt.WorkspacePath, |
| 246 | }) |
| 247 | } |
| 248 | return manifest, nil |
| 249 | } |
| 250 | |
| 251 | func PrintBlameFile(out io.Writer, manifest FileBlameManifest) { |