gomcp/internal/domain/memory/fact.go

244 lines
6.9 KiB
Go

// Package memory defines domain entities for hierarchical memory (H-MEM).
package memory
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"sort"
"time"
)
// HierLevel represents a hierarchical memory level (L0-L3).
type HierLevel int
const (
LevelProject HierLevel = 0 // L0: architecture, Iron Laws, project-wide
LevelDomain HierLevel = 1 // L1: feature areas, component boundaries
LevelModule HierLevel = 2 // L2: function interfaces, dependencies
LevelSnippet HierLevel = 3 // L3: raw messages, code diffs, episodes
)
// String returns human-readable level name.
func (l HierLevel) String() string {
switch l {
case LevelProject:
return "project"
case LevelDomain:
return "domain"
case LevelModule:
return "module"
case LevelSnippet:
return "snippet"
default:
return "unknown"
}
}
// IsValid checks if the level is within valid range.
func (l HierLevel) IsValid() bool {
return l >= LevelProject && l <= LevelSnippet
}
// HierLevelFromInt converts an integer to HierLevel with validation.
func HierLevelFromInt(i int) (HierLevel, bool) {
l := HierLevel(i)
if !l.IsValid() {
return 0, false
}
return l, true
}
// TTL expiry policies.
const (
OnExpireMarkStale = "mark_stale"
OnExpireArchive = "archive"
OnExpireDelete = "delete"
)
// TTLConfig defines time-to-live configuration for a fact.
type TTLConfig struct {
TTLSeconds int `json:"ttl_seconds"`
RefreshTrigger string `json:"refresh_trigger,omitempty"` // file path that refreshes TTL
OnExpire string `json:"on_expire"` // mark_stale | archive | delete
}
// IsExpired checks if the TTL has expired relative to createdAt.
func (t *TTLConfig) IsExpired(createdAt time.Time) bool {
if t.TTLSeconds <= 0 {
return false // zero or negative TTL = never expires
}
return time.Since(createdAt) > time.Duration(t.TTLSeconds)*time.Second
}
// Validate checks TTLConfig fields.
func (t *TTLConfig) Validate() error {
if t.TTLSeconds < 0 {
return errors.New("ttl_seconds must be non-negative")
}
switch t.OnExpire {
case OnExpireMarkStale, OnExpireArchive, OnExpireDelete:
return nil
default:
return errors.New("on_expire must be mark_stale, archive, or delete")
}
}
// ErrImmutableFact is returned when attempting to mutate a gene (immutable fact).
var ErrImmutableFact = errors.New("cannot mutate gene: immutable fact")
// Fact represents a hierarchical memory fact.
// Compatible with memory_bridge_v2.db hierarchical_facts table.
type Fact struct {
ID string `json:"id"`
Content string `json:"content"`
Level HierLevel `json:"level"`
Domain string `json:"domain,omitempty"`
Module string `json:"module,omitempty"`
CodeRef string `json:"code_ref,omitempty"` // file:line
ParentID string `json:"parent_id,omitempty"`
IsStale bool `json:"is_stale"`
IsArchived bool `json:"is_archived"`
IsGene bool `json:"is_gene"` // Genome Layer: immutable survival invariant
Confidence float64 `json:"confidence"`
Source string `json:"source"` // "manual" | "consolidation" | "genome" | etc.
SessionID string `json:"session_id,omitempty"`
TTL *TTLConfig `json:"ttl,omitempty"`
Embedding []float64 `json:"embedding,omitempty"` // JSON-encoded in DB
HitCount int `json:"hit_count"` // v3.3: context access counter
LastAccess time.Time `json:"last_accessed_at"` // v3.3: last context inclusion
CreatedAt time.Time `json:"created_at"`
ValidFrom time.Time `json:"valid_from"`
ValidUntil *time.Time `json:"valid_until,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
}
// NewFact creates a new Fact with a generated ID and timestamps.
func NewFact(content string, level HierLevel, domain, module string) *Fact {
now := time.Now()
return &Fact{
ID: generateID(),
Content: content,
Level: level,
Domain: domain,
Module: module,
IsStale: false,
IsArchived: false,
IsGene: false,
Confidence: 1.0,
Source: "manual",
CreatedAt: now,
ValidFrom: now,
UpdatedAt: now,
}
}
// NewGene creates an immutable genome fact (L0 only).
// Genes are survival invariants that cannot be updated or deleted.
func NewGene(content string, domain string) *Fact {
now := time.Now()
return &Fact{
ID: generateID(),
Content: content,
Level: LevelProject,
Domain: domain,
IsStale: false,
IsArchived: false,
IsGene: true,
Confidence: 1.0,
Source: "genome",
CreatedAt: now,
ValidFrom: now,
UpdatedAt: now,
}
}
// IsImmutable returns true if this fact is a gene and cannot be mutated.
func (f *Fact) IsImmutable() bool {
return f.IsGene
}
// Validate checks required fields and constraints.
func (f *Fact) Validate() error {
if f.ID == "" {
return errors.New("fact ID is required")
}
if f.Content == "" {
return errors.New("fact content is required")
}
if !f.Level.IsValid() {
return errors.New("invalid hierarchy level")
}
if f.TTL != nil {
if err := f.TTL.Validate(); err != nil {
return err
}
}
return nil
}
// HasEmbedding returns true if the fact has a vector embedding.
func (f *Fact) HasEmbedding() bool {
return len(f.Embedding) > 0
}
// MarkStale marks the fact as stale.
func (f *Fact) MarkStale() {
f.IsStale = true
f.UpdatedAt = time.Now()
}
// Archive marks the fact as archived.
func (f *Fact) Archive() {
f.IsArchived = true
f.UpdatedAt = time.Now()
}
// SetValidUntil sets the valid_until timestamp.
func (f *Fact) SetValidUntil(t time.Time) {
f.ValidUntil = &t
f.UpdatedAt = time.Now()
}
// FactStoreStats holds aggregate statistics about the fact store.
type FactStoreStats struct {
TotalFacts int `json:"total_facts"`
ByLevel map[HierLevel]int `json:"by_level"`
ByDomain map[string]int `json:"by_domain"`
StaleCount int `json:"stale_count"`
WithEmbeddings int `json:"with_embeddings"`
GeneCount int `json:"gene_count"`
ColdCount int `json:"cold_count"` // v3.3: hit_count=0, >30d
GenomeHash string `json:"genome_hash,omitempty"` // Merkle root of all genes
}
// GenomeHash computes a deterministic hash of all gene facts.
// This serves as a Merkle-style integrity verification for the Genome Layer.
func GenomeHash(genes []*Fact) string {
if len(genes) == 0 {
return ""
}
// Sort by ID for deterministic ordering.
sorted := make([]*Fact, len(genes))
copy(sorted, genes)
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].ID < sorted[j].ID
})
// Build Merkle leaf hashes.
h := sha256.New()
for _, g := range sorted {
leaf := sha256.Sum256([]byte(fmt.Sprintf("%s:%s", g.ID, g.Content)))
h.Write(leaf[:])
}
return hex.EncodeToString(h.Sum(nil))
}
// generateID creates a random 16-byte hex ID.
func generateID() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}