mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-05 23:03:55 +00:00
201 lines
5.7 KiB
Go
201 lines
5.7 KiB
Go
// Package sdk provides SDK agent integration for claude-mnemonic.
|
|
package sdk
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/lukaszraczylo/claude-mnemonic/pkg/models"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
var (
|
|
// Observation parsing
|
|
observationRegex = regexp.MustCompile(`(?s)<observation>(.*?)</observation>`)
|
|
|
|
// Summary parsing
|
|
summaryRegex = regexp.MustCompile(`(?s)<summary>(.*?)</summary>`)
|
|
skipSummaryRegex = regexp.MustCompile(`<skip_summary\s+reason="([^"]+)"\s*/>`)
|
|
|
|
// Valid observation types
|
|
validObsTypes = map[string]bool{
|
|
"bugfix": true,
|
|
"feature": true,
|
|
"refactor": true,
|
|
"change": true,
|
|
"discovery": true,
|
|
"decision": true,
|
|
}
|
|
|
|
// Valid concepts - expanded list matching GlobalizableConcepts and common use cases
|
|
validConcepts = map[string]bool{
|
|
// Semantic concepts
|
|
"how-it-works": true,
|
|
"why-it-exists": true,
|
|
"what-changed": true,
|
|
"problem-solution": true,
|
|
"gotcha": true,
|
|
"pattern": true,
|
|
"trade-off": true,
|
|
// Globalizable concepts (from models.GlobalizableConcepts)
|
|
"best-practice": true,
|
|
"anti-pattern": true,
|
|
"architecture": true,
|
|
"security": true,
|
|
"performance": true,
|
|
"testing": true,
|
|
"debugging": true,
|
|
"workflow": true,
|
|
"tooling": true,
|
|
// Additional useful concepts
|
|
"refactoring": true,
|
|
"api": true,
|
|
"database": true,
|
|
"configuration": true,
|
|
"error-handling": true,
|
|
"caching": true,
|
|
"logging": true,
|
|
"auth": true,
|
|
"validation": true,
|
|
}
|
|
)
|
|
|
|
// ParseObservations parses observation XML blocks from SDK response text.
|
|
func ParseObservations(text string, correlationID string) []*models.ParsedObservation {
|
|
var observations []*models.ParsedObservation
|
|
|
|
matches := observationRegex.FindAllStringSubmatch(text, -1)
|
|
for _, match := range matches {
|
|
if len(match) < 2 {
|
|
continue
|
|
}
|
|
|
|
obsContent := match[1]
|
|
|
|
// Extract fields
|
|
obsType := extractField(obsContent, "type")
|
|
title := extractField(obsContent, "title")
|
|
subtitle := extractField(obsContent, "subtitle")
|
|
narrative := extractField(obsContent, "narrative")
|
|
facts := extractArrayElements(obsContent, "facts", "fact")
|
|
concepts := extractArrayElements(obsContent, "concepts", "concept")
|
|
filesRead := extractArrayElements(obsContent, "files_read", "file")
|
|
filesModified := extractArrayElements(obsContent, "files_modified", "file")
|
|
|
|
// Determine final type (default to "change" if invalid)
|
|
finalType := models.ObsTypeChange
|
|
if obsType != "" {
|
|
if validObsTypes[obsType] {
|
|
finalType = models.ObservationType(obsType)
|
|
} else {
|
|
log.Warn().
|
|
Str("correlationId", correlationID).
|
|
Str("invalidType", obsType).
|
|
Msg("Invalid observation type, using 'change'")
|
|
}
|
|
} else {
|
|
log.Warn().
|
|
Str("correlationId", correlationID).
|
|
Msg("Observation missing type field, using 'change'")
|
|
}
|
|
|
|
// Filter concepts: only keep valid ones from the strict list
|
|
cleanedConcepts := make([]string, 0, len(concepts))
|
|
var invalidConcepts []string
|
|
for _, c := range concepts {
|
|
c = strings.ToLower(strings.TrimSpace(c))
|
|
if c == string(finalType) {
|
|
continue // Skip type in concepts
|
|
}
|
|
if validConcepts[c] {
|
|
cleanedConcepts = append(cleanedConcepts, c)
|
|
} else {
|
|
invalidConcepts = append(invalidConcepts, c)
|
|
}
|
|
}
|
|
if len(invalidConcepts) > 0 {
|
|
log.Warn().
|
|
Str("correlationId", correlationID).
|
|
Strs("invalidConcepts", invalidConcepts).
|
|
Msg("Filtered out invalid concepts (not in allowed list)")
|
|
}
|
|
|
|
observations = append(observations, &models.ParsedObservation{
|
|
Type: finalType,
|
|
Title: title,
|
|
Subtitle: subtitle,
|
|
Facts: facts,
|
|
Narrative: narrative,
|
|
Concepts: cleanedConcepts,
|
|
FilesRead: filesRead,
|
|
FilesModified: filesModified,
|
|
})
|
|
}
|
|
|
|
return observations
|
|
}
|
|
|
|
// ParseSummary parses a summary XML block from SDK response text.
|
|
func ParseSummary(text string, sessionID int64) *models.ParsedSummary {
|
|
// Check for skip_summary first
|
|
if skipMatch := skipSummaryRegex.FindStringSubmatch(text); skipMatch != nil {
|
|
log.Info().
|
|
Int64("sessionId", sessionID).
|
|
Str("reason", skipMatch[1]).
|
|
Msg("Summary skipped")
|
|
return nil
|
|
}
|
|
|
|
// Find summary block
|
|
match := summaryRegex.FindStringSubmatch(text)
|
|
if len(match) < 2 {
|
|
return nil
|
|
}
|
|
|
|
summaryContent := match[1]
|
|
|
|
return &models.ParsedSummary{
|
|
Request: extractField(summaryContent, "request"),
|
|
Investigated: extractField(summaryContent, "investigated"),
|
|
Learned: extractField(summaryContent, "learned"),
|
|
Completed: extractField(summaryContent, "completed"),
|
|
NextSteps: extractField(summaryContent, "next_steps"),
|
|
Notes: extractField(summaryContent, "notes"),
|
|
}
|
|
}
|
|
|
|
// extractField extracts a simple field value from XML content.
|
|
func extractField(content, fieldName string) string {
|
|
pattern := regexp.MustCompile(`<` + fieldName + `>([^<]*)</` + fieldName + `>`)
|
|
match := pattern.FindStringSubmatch(content)
|
|
if len(match) < 2 {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(match[1])
|
|
}
|
|
|
|
// extractArrayElements extracts array elements from XML content.
|
|
func extractArrayElements(content, arrayName, elementName string) []string {
|
|
var elements []string
|
|
|
|
// Find the array block
|
|
arrayPattern := regexp.MustCompile(`(?s)<` + arrayName + `>(.*?)</` + arrayName + `>`)
|
|
arrayMatch := arrayPattern.FindStringSubmatch(content)
|
|
if len(arrayMatch) < 2 {
|
|
return elements
|
|
}
|
|
|
|
arrayContent := arrayMatch[1]
|
|
|
|
// Extract individual elements
|
|
elementPattern := regexp.MustCompile(`<` + elementName + `>([^<]+)</` + elementName + `>`)
|
|
elementMatches := elementPattern.FindAllStringSubmatch(arrayContent, -1)
|
|
for _, match := range elementMatches {
|
|
if len(match) >= 2 {
|
|
elements = append(elements, strings.TrimSpace(match[1]))
|
|
}
|
|
}
|
|
|
|
return elements
|
|
}
|