mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-25 04:16:22 +02:00
244 lines
6.9 KiB
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)
|
|
}
|