mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-09 23:59:40 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
// Package models contains domain models for claude-mnemonic.
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ObservationType represents the type of observation.
|
||||
type ObservationType string
|
||||
|
||||
const (
|
||||
ObsTypeDecision ObservationType = "decision"
|
||||
ObsTypeBugfix ObservationType = "bugfix"
|
||||
ObsTypeFeature ObservationType = "feature"
|
||||
ObsTypeRefactor ObservationType = "refactor"
|
||||
ObsTypeDiscovery ObservationType = "discovery"
|
||||
ObsTypeChange ObservationType = "change"
|
||||
)
|
||||
|
||||
// ObservationScope defines the visibility scope of an observation.
|
||||
type ObservationScope string
|
||||
|
||||
const (
|
||||
// ScopeProject means the observation is only visible within the same project.
|
||||
ScopeProject ObservationScope = "project"
|
||||
// ScopeGlobal means the observation is visible across all projects.
|
||||
// Used for best practices, advanced patterns, and generalizable knowledge.
|
||||
ScopeGlobal ObservationScope = "global"
|
||||
)
|
||||
|
||||
// GlobalizableConcepts are concept tags that indicate an observation
|
||||
// should be considered for global scope (best practices, patterns, etc.)
|
||||
var GlobalizableConcepts = []string{
|
||||
"best-practice",
|
||||
"pattern",
|
||||
"anti-pattern",
|
||||
"architecture",
|
||||
"security",
|
||||
"performance",
|
||||
"testing",
|
||||
"debugging",
|
||||
"workflow",
|
||||
"tooling",
|
||||
}
|
||||
|
||||
// JSONStringArray is a custom type for handling JSON string arrays in SQLite.
|
||||
type JSONStringArray []string
|
||||
|
||||
// Scan implements sql.Scanner for JSONStringArray.
|
||||
func (j *JSONStringArray) Scan(src interface{}) error {
|
||||
if src == nil {
|
||||
*j = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
var data []byte
|
||||
switch v := src.(type) {
|
||||
case string:
|
||||
data = []byte(v)
|
||||
case []byte:
|
||||
data = v
|
||||
default:
|
||||
return fmt.Errorf("JSONStringArray: unsupported type %T", src)
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
*j = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, j)
|
||||
}
|
||||
|
||||
// Value implements driver.Valuer for JSONStringArray.
|
||||
func (j JSONStringArray) Value() (driver.Value, error) {
|
||||
if j == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return json.Marshal(j)
|
||||
}
|
||||
|
||||
// JSONInt64Map is a custom type for handling JSON int64 maps in SQLite.
|
||||
type JSONInt64Map map[string]int64
|
||||
|
||||
// Scan implements sql.Scanner for JSONInt64Map.
|
||||
func (j *JSONInt64Map) Scan(src interface{}) error {
|
||||
if src == nil {
|
||||
*j = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
var data []byte
|
||||
switch v := src.(type) {
|
||||
case string:
|
||||
data = []byte(v)
|
||||
case []byte:
|
||||
data = v
|
||||
default:
|
||||
return fmt.Errorf("JSONInt64Map: unsupported type %T", src)
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
*j = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, j)
|
||||
}
|
||||
|
||||
// Value implements driver.Valuer for JSONInt64Map.
|
||||
func (j JSONInt64Map) Value() (driver.Value, error) {
|
||||
if j == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return json.Marshal(j)
|
||||
}
|
||||
|
||||
// Observation represents a learning extracted from a Claude Code session.
|
||||
type Observation struct {
|
||||
ID int64 `db:"id" json:"id"`
|
||||
SDKSessionID string `db:"sdk_session_id" json:"sdk_session_id"`
|
||||
Project string `db:"project" json:"project"`
|
||||
Scope ObservationScope `db:"scope" json:"scope"`
|
||||
Type ObservationType `db:"type" json:"type"`
|
||||
Title sql.NullString `db:"title" json:"title,omitempty"`
|
||||
Subtitle sql.NullString `db:"subtitle" json:"subtitle,omitempty"`
|
||||
Facts JSONStringArray `db:"facts" json:"facts,omitempty"`
|
||||
Narrative sql.NullString `db:"narrative" json:"narrative,omitempty"`
|
||||
Concepts JSONStringArray `db:"concepts" json:"concepts,omitempty"`
|
||||
FilesRead JSONStringArray `db:"files_read" json:"files_read,omitempty"`
|
||||
FilesModified JSONStringArray `db:"files_modified" json:"files_modified,omitempty"`
|
||||
FileMtimes JSONInt64Map `db:"file_mtimes" json:"file_mtimes,omitempty"`
|
||||
PromptNumber sql.NullInt64 `db:"prompt_number" json:"prompt_number,omitempty"`
|
||||
DiscoveryTokens int64 `db:"discovery_tokens" json:"discovery_tokens"`
|
||||
CreatedAt string `db:"created_at" json:"created_at"`
|
||||
CreatedAtEpoch int64 `db:"created_at_epoch" json:"created_at_epoch"`
|
||||
IsStale bool `db:"-" json:"is_stale,omitempty"`
|
||||
}
|
||||
|
||||
// ParsedObservation represents an observation parsed from SDK response XML.
|
||||
type ParsedObservation struct {
|
||||
Type ObservationType
|
||||
Title string
|
||||
Subtitle string
|
||||
Facts []string
|
||||
Narrative string
|
||||
Concepts []string
|
||||
FilesRead []string
|
||||
FilesModified []string
|
||||
FileMtimes map[string]int64 // File path -> mtime epoch ms
|
||||
Scope ObservationScope // Optional: if empty, will be auto-determined
|
||||
}
|
||||
|
||||
// ToStoredObservation converts a ParsedObservation to the stored Observation format.
|
||||
// Used for similarity comparison before storage.
|
||||
func (p *ParsedObservation) ToStoredObservation() *Observation {
|
||||
return &Observation{
|
||||
Type: p.Type,
|
||||
Title: sql.NullString{String: p.Title, Valid: p.Title != ""},
|
||||
Subtitle: sql.NullString{String: p.Subtitle, Valid: p.Subtitle != ""},
|
||||
Facts: p.Facts,
|
||||
Narrative: sql.NullString{String: p.Narrative, Valid: p.Narrative != ""},
|
||||
Concepts: p.Concepts,
|
||||
FilesRead: p.FilesRead,
|
||||
FilesModified: p.FilesModified,
|
||||
FileMtimes: p.FileMtimes,
|
||||
}
|
||||
}
|
||||
|
||||
// DetermineScope determines the appropriate scope based on observation concepts.
|
||||
// Returns ScopeGlobal if any concept matches globalizable patterns, else ScopeProject.
|
||||
func DetermineScope(concepts []string) ObservationScope {
|
||||
for _, concept := range concepts {
|
||||
for _, globalConcept := range GlobalizableConcepts {
|
||||
if concept == globalConcept {
|
||||
return ScopeGlobal
|
||||
}
|
||||
}
|
||||
}
|
||||
return ScopeProject
|
||||
}
|
||||
|
||||
// ObservationJSON is a JSON-friendly representation of Observation.
|
||||
// It converts sql.NullString to plain strings for clean JSON output.
|
||||
type ObservationJSON struct {
|
||||
ID int64 `json:"id"`
|
||||
SDKSessionID string `json:"sdk_session_id"`
|
||||
Project string `json:"project"`
|
||||
Scope ObservationScope `json:"scope"`
|
||||
Type ObservationType `json:"type"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Subtitle string `json:"subtitle,omitempty"`
|
||||
Facts []string `json:"facts,omitempty"`
|
||||
Narrative string `json:"narrative,omitempty"`
|
||||
Concepts []string `json:"concepts,omitempty"`
|
||||
FilesRead []string `json:"files_read,omitempty"`
|
||||
FilesModified []string `json:"files_modified,omitempty"`
|
||||
FileMtimes map[string]int64 `json:"file_mtimes,omitempty"`
|
||||
PromptNumber int64 `json:"prompt_number,omitempty"`
|
||||
DiscoveryTokens int64 `json:"discovery_tokens"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
CreatedAtEpoch int64 `json:"created_at_epoch"`
|
||||
IsStale bool `json:"is_stale,omitempty"`
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler for Observation.
|
||||
// Converts sql.NullString fields to plain strings.
|
||||
func (o *Observation) MarshalJSON() ([]byte, error) {
|
||||
j := ObservationJSON{
|
||||
ID: o.ID,
|
||||
SDKSessionID: o.SDKSessionID,
|
||||
Project: o.Project,
|
||||
Scope: o.Scope,
|
||||
Type: o.Type,
|
||||
Facts: o.Facts,
|
||||
Concepts: o.Concepts,
|
||||
FilesRead: o.FilesRead,
|
||||
FilesModified: o.FilesModified,
|
||||
FileMtimes: o.FileMtimes,
|
||||
DiscoveryTokens: o.DiscoveryTokens,
|
||||
CreatedAt: o.CreatedAt,
|
||||
CreatedAtEpoch: o.CreatedAtEpoch,
|
||||
IsStale: o.IsStale,
|
||||
}
|
||||
if o.Title.Valid {
|
||||
j.Title = o.Title.String
|
||||
}
|
||||
if o.Subtitle.Valid {
|
||||
j.Subtitle = o.Subtitle.String
|
||||
}
|
||||
if o.Narrative.Valid {
|
||||
j.Narrative = o.Narrative.String
|
||||
}
|
||||
if o.PromptNumber.Valid {
|
||||
j.PromptNumber = o.PromptNumber.Int64
|
||||
}
|
||||
return json.Marshal(j)
|
||||
}
|
||||
|
||||
// NewObservation creates a new observation from parsed data.
|
||||
func NewObservation(sdkSessionID, project string, parsed *ParsedObservation, promptNumber int, discoveryTokens int64) *Observation {
|
||||
now := time.Now()
|
||||
|
||||
// Determine scope: use parsed scope if set, otherwise auto-determine from concepts
|
||||
scope := parsed.Scope
|
||||
if scope == "" {
|
||||
scope = DetermineScope(parsed.Concepts)
|
||||
}
|
||||
|
||||
return &Observation{
|
||||
SDKSessionID: sdkSessionID,
|
||||
Project: project,
|
||||
Scope: scope,
|
||||
Type: parsed.Type,
|
||||
Title: sql.NullString{String: parsed.Title, Valid: parsed.Title != ""},
|
||||
Subtitle: sql.NullString{String: parsed.Subtitle, Valid: parsed.Subtitle != ""},
|
||||
Facts: parsed.Facts,
|
||||
Narrative: sql.NullString{String: parsed.Narrative, Valid: parsed.Narrative != ""},
|
||||
Concepts: parsed.Concepts,
|
||||
FilesRead: parsed.FilesRead,
|
||||
FilesModified: parsed.FilesModified,
|
||||
FileMtimes: parsed.FileMtimes,
|
||||
PromptNumber: sql.NullInt64{Int64: int64(promptNumber), Valid: promptNumber > 0},
|
||||
DiscoveryTokens: discoveryTokens,
|
||||
CreatedAt: now.Format(time.RFC3339),
|
||||
CreatedAtEpoch: now.UnixMilli(),
|
||||
}
|
||||
}
|
||||
|
||||
// CheckStaleness checks if an observation is stale based on current file mtimes.
|
||||
// Returns true if any tracked file has been modified since the observation was created.
|
||||
func (o *Observation) CheckStaleness(currentMtimes map[string]int64) bool {
|
||||
if len(o.FileMtimes) == 0 {
|
||||
return false // No file tracking, assume fresh
|
||||
}
|
||||
|
||||
for path, recordedMtime := range o.FileMtimes {
|
||||
if currentMtime, exists := currentMtimes[path]; exists {
|
||||
if currentMtime > recordedMtime {
|
||||
return true // File was modified since observation was created
|
||||
}
|
||||
}
|
||||
// If file doesn't exist in currentMtimes, it may have been deleted
|
||||
// We don't mark as stale for missing files - they might just not be checked
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Package models contains domain models for claude-mnemonic.
|
||||
package models
|
||||
|
||||
// UserPrompt represents a user prompt captured during a session.
|
||||
type UserPrompt struct {
|
||||
ID int64 `db:"id" json:"id"`
|
||||
ClaudeSessionID string `db:"claude_session_id" json:"claude_session_id"`
|
||||
PromptNumber int `db:"prompt_number" json:"prompt_number"`
|
||||
PromptText string `db:"prompt_text" json:"prompt_text"`
|
||||
MatchedObservations int `db:"matched_observations" json:"matched_observations"`
|
||||
CreatedAt string `db:"created_at" json:"created_at"`
|
||||
CreatedAtEpoch int64 `db:"created_at_epoch" json:"created_at_epoch"`
|
||||
}
|
||||
|
||||
// UserPromptWithSession includes session context for search results.
|
||||
type UserPromptWithSession struct {
|
||||
UserPrompt
|
||||
Project string `db:"project" json:"project"`
|
||||
SDKSessionID string `db:"sdk_session_id" json:"sdk_session_id"`
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Package models contains domain models for claude-mnemonic.
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SessionStatus represents the status of an SDK session.
|
||||
type SessionStatus string
|
||||
|
||||
const (
|
||||
SessionStatusActive SessionStatus = "active"
|
||||
SessionStatusCompleted SessionStatus = "completed"
|
||||
SessionStatusFailed SessionStatus = "failed"
|
||||
)
|
||||
|
||||
// SDKSession represents a Claude Code session tracked by the memory system.
|
||||
type SDKSession struct {
|
||||
ID int64 `db:"id" json:"id"`
|
||||
ClaudeSessionID string `db:"claude_session_id" json:"claude_session_id"`
|
||||
SDKSessionID sql.NullString `db:"sdk_session_id" json:"sdk_session_id,omitempty"`
|
||||
Project string `db:"project" json:"project"`
|
||||
UserPrompt sql.NullString `db:"user_prompt" json:"user_prompt,omitempty"`
|
||||
WorkerPort sql.NullInt64 `db:"worker_port" json:"worker_port,omitempty"`
|
||||
PromptCounter int64 `db:"prompt_counter" json:"prompt_counter"`
|
||||
Status SessionStatus `db:"status" json:"status"`
|
||||
StartedAt string `db:"started_at" json:"started_at"`
|
||||
StartedAtEpoch int64 `db:"started_at_epoch" json:"started_at_epoch"`
|
||||
CompletedAt sql.NullString `db:"completed_at" json:"completed_at,omitempty"`
|
||||
CompletedAtEpoch sql.NullInt64 `db:"completed_at_epoch" json:"completed_at_epoch,omitempty"`
|
||||
}
|
||||
|
||||
// ActiveSession represents an in-memory active session being processed.
|
||||
type ActiveSession struct {
|
||||
SessionDBID int64
|
||||
ClaudeSessionID string
|
||||
SDKSessionID string
|
||||
Project string
|
||||
UserPrompt string
|
||||
LastPromptNumber int
|
||||
StartTime time.Time
|
||||
CumulativeInputTokens int64
|
||||
CumulativeOutputTokens int64
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Package models contains domain models for claude-mnemonic.
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SessionSummary represents a summary of a Claude Code session.
|
||||
type SessionSummary struct {
|
||||
ID int64 `db:"id" json:"id"`
|
||||
SDKSessionID string `db:"sdk_session_id" json:"sdk_session_id"`
|
||||
Project string `db:"project" json:"project"`
|
||||
Request sql.NullString `db:"request" json:"request,omitempty"`
|
||||
Investigated sql.NullString `db:"investigated" json:"investigated,omitempty"`
|
||||
Learned sql.NullString `db:"learned" json:"learned,omitempty"`
|
||||
Completed sql.NullString `db:"completed" json:"completed,omitempty"`
|
||||
NextSteps sql.NullString `db:"next_steps" json:"next_steps,omitempty"`
|
||||
Notes sql.NullString `db:"notes" json:"notes,omitempty"`
|
||||
PromptNumber sql.NullInt64 `db:"prompt_number" json:"prompt_number,omitempty"`
|
||||
DiscoveryTokens int64 `db:"discovery_tokens" json:"discovery_tokens"`
|
||||
CreatedAt string `db:"created_at" json:"created_at"`
|
||||
CreatedAtEpoch int64 `db:"created_at_epoch" json:"created_at_epoch"`
|
||||
}
|
||||
|
||||
// ParsedSummary represents a summary parsed from SDK response XML.
|
||||
type ParsedSummary struct {
|
||||
Request string
|
||||
Investigated string
|
||||
Learned string
|
||||
Completed string
|
||||
NextSteps string
|
||||
Notes string
|
||||
}
|
||||
|
||||
// NewSessionSummary creates a new session summary from parsed data.
|
||||
func NewSessionSummary(sdkSessionID, project string, parsed *ParsedSummary, promptNumber int, discoveryTokens int64) *SessionSummary {
|
||||
now := time.Now()
|
||||
return &SessionSummary{
|
||||
SDKSessionID: sdkSessionID,
|
||||
Project: project,
|
||||
Request: sql.NullString{String: parsed.Request, Valid: parsed.Request != ""},
|
||||
Investigated: sql.NullString{String: parsed.Investigated, Valid: parsed.Investigated != ""},
|
||||
Learned: sql.NullString{String: parsed.Learned, Valid: parsed.Learned != ""},
|
||||
Completed: sql.NullString{String: parsed.Completed, Valid: parsed.Completed != ""},
|
||||
NextSteps: sql.NullString{String: parsed.NextSteps, Valid: parsed.NextSteps != ""},
|
||||
Notes: sql.NullString{String: parsed.Notes, Valid: parsed.Notes != ""},
|
||||
PromptNumber: sql.NullInt64{Int64: int64(promptNumber), Valid: promptNumber > 0},
|
||||
DiscoveryTokens: discoveryTokens,
|
||||
CreatedAt: now.Format(time.RFC3339),
|
||||
CreatedAtEpoch: now.UnixMilli(),
|
||||
}
|
||||
}
|
||||
|
||||
// SessionSummaryJSON is a JSON-friendly representation of SessionSummary.
|
||||
// It converts sql.NullString to plain strings for clean JSON output.
|
||||
type SessionSummaryJSON struct {
|
||||
ID int64 `json:"id"`
|
||||
SDKSessionID string `json:"sdk_session_id"`
|
||||
Project string `json:"project"`
|
||||
Request string `json:"request,omitempty"`
|
||||
Investigated string `json:"investigated,omitempty"`
|
||||
Learned string `json:"learned,omitempty"`
|
||||
Completed string `json:"completed,omitempty"`
|
||||
NextSteps string `json:"next_steps,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
PromptNumber int64 `json:"prompt_number,omitempty"`
|
||||
DiscoveryTokens int64 `json:"discovery_tokens"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
CreatedAtEpoch int64 `json:"created_at_epoch"`
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler for SessionSummary.
|
||||
// Converts sql.NullString fields to plain strings.
|
||||
func (s *SessionSummary) MarshalJSON() ([]byte, error) {
|
||||
j := SessionSummaryJSON{
|
||||
ID: s.ID,
|
||||
SDKSessionID: s.SDKSessionID,
|
||||
Project: s.Project,
|
||||
DiscoveryTokens: s.DiscoveryTokens,
|
||||
CreatedAt: s.CreatedAt,
|
||||
CreatedAtEpoch: s.CreatedAtEpoch,
|
||||
}
|
||||
if s.Request.Valid {
|
||||
j.Request = s.Request.String
|
||||
}
|
||||
if s.Investigated.Valid {
|
||||
j.Investigated = s.Investigated.String
|
||||
}
|
||||
if s.Learned.Valid {
|
||||
j.Learned = s.Learned.String
|
||||
}
|
||||
if s.Completed.Valid {
|
||||
j.Completed = s.Completed.String
|
||||
}
|
||||
if s.NextSteps.Valid {
|
||||
j.NextSteps = s.NextSteps.String
|
||||
}
|
||||
if s.Notes.Valid {
|
||||
j.Notes = s.Notes.String
|
||||
}
|
||||
if s.PromptNumber.Valid {
|
||||
j.PromptNumber = s.PromptNumber.Int64
|
||||
}
|
||||
return json.Marshal(j)
|
||||
}
|
||||
Reference in New Issue
Block a user