158 lines
3.5 KiB
Go
158 lines
3.5 KiB
Go
package ingest
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type jsonKind int
|
|
|
|
const (
|
|
jsonObject jsonKind = iota
|
|
jsonArray
|
|
)
|
|
|
|
var _ Ingester = (*SessionDiffIngester)(nil)
|
|
|
|
type SessionDiffIngester struct {
|
|
Version string
|
|
}
|
|
|
|
func NewSessionDiffIngester() *SessionDiffIngester {
|
|
return &SessionDiffIngester{Version: "session-diff/v1"}
|
|
}
|
|
|
|
func (s *SessionDiffIngester) SourceID() string { return "opencode-session" }
|
|
|
|
func (s *SessionDiffIngester) Ingest(path string) (*IngestResult, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read session diff: %w", err)
|
|
}
|
|
if len(data) < 10 {
|
|
return nil, nil
|
|
}
|
|
|
|
raw, kind := parseJSON(data)
|
|
if raw == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
sessionID := guessString(raw, "session_id", "id", "sessionId")
|
|
project := guessString(raw, "project", "project_id", "projectPath", "projectId")
|
|
title := guessString(raw, "title", "name", "label")
|
|
createdAt := guessString(raw, "created_at", "timestamp", "date", "started_at")
|
|
if createdAt == "" {
|
|
if fi, err := os.Stat(path); err == nil {
|
|
createdAt = fi.ModTime().UTC().Format(time.RFC3339)
|
|
}
|
|
}
|
|
|
|
if sessionID == "" {
|
|
base := filepath.Base(path)
|
|
sessionID = strings.TrimSuffix(base, filepath.Ext(base))
|
|
sessionID = strings.TrimPrefix(sessionID, "ses_")
|
|
}
|
|
if title == "" {
|
|
title = fmt.Sprintf("Session %s", sessionID[:min(8, len(sessionID))])
|
|
}
|
|
|
|
summary := extractSummary(raw, kind)
|
|
fp := FingerprintWithMeta(data, map[string]string{
|
|
"session_id": sessionID,
|
|
"source": "session_diff",
|
|
})
|
|
|
|
confidence := 0.7
|
|
if kind == jsonArray {
|
|
confidence = 0.4
|
|
}
|
|
|
|
return &IngestResult{
|
|
Fingerprint: fp,
|
|
SourceID: s.SourceID(),
|
|
SourcePath: path,
|
|
Project: project,
|
|
ContentType: ".json",
|
|
Title: title,
|
|
Summary: summary,
|
|
CreatedAt: createdAt,
|
|
LineStart: 0,
|
|
LineEnd: 0,
|
|
Confidence: confidence,
|
|
IngesterVersion: s.Version,
|
|
Provenance: map[string]any{
|
|
"session_id": sessionID,
|
|
"kind": kindName(kind),
|
|
"field_count": len(raw),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func parseJSON(data []byte) (map[string]any, jsonKind) {
|
|
var obj map[string]any
|
|
if err := json.Unmarshal(data, &obj); err == nil {
|
|
return obj, jsonObject
|
|
}
|
|
var arr []any
|
|
if err := json.Unmarshal(data, &arr); err == nil {
|
|
m := make(map[string]any)
|
|
m["_count"] = float64(len(arr))
|
|
m["_kind"] = "array"
|
|
if len(arr) > 0 {
|
|
if first, ok := arr[0].(map[string]any); ok {
|
|
for k, v := range first {
|
|
if str, ok := v.(string); ok && len(str) < 200 {
|
|
m["_sample_"+k] = str
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return m, jsonArray
|
|
}
|
|
return nil, jsonObject
|
|
}
|
|
|
|
func extractSummary(raw map[string]any, kind jsonKind) string {
|
|
if msg, ok := raw["message"].(string); ok && len(msg) > 0 {
|
|
return truncate(msg, 200)
|
|
}
|
|
if msgs, ok := raw["messages"].([]any); ok && len(msgs) > 0 {
|
|
parts := make([]string, 0, len(msgs))
|
|
for _, m := range msgs {
|
|
if mm, ok := m.(map[string]any); ok {
|
|
if c, ok := mm["content"].(string); ok {
|
|
parts = append(parts, truncate(c, 100))
|
|
}
|
|
}
|
|
}
|
|
return strings.Join(parts, " | ")
|
|
}
|
|
if count, ok := raw["_count"].(float64); ok && kind == jsonArray {
|
|
return fmt.Sprintf("Array with %.0f entries", count)
|
|
}
|
|
b, _ := json.Marshal(raw)
|
|
return truncate(string(b), 200)
|
|
}
|
|
|
|
func guessString(m map[string]any, keys ...string) string {
|
|
for _, k := range keys {
|
|
if v, ok := m[k].(string); ok && v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func kindName(k jsonKind) string {
|
|
if k == jsonArray {
|
|
return "array"
|
|
}
|
|
return "object"
|
|
}
|