initial: Syntrex extraction from sentinel-community (615 files)

This commit is contained in:
DmitrL-dev 2026-03-11 15:12:02 +10:00
commit 2c50c993b1
175 changed files with 32396 additions and 0 deletions

View file

@ -0,0 +1,181 @@
// Package tools — Apathy Detection and Apoptosis Recovery (DIP H1.4).
//
// This file implements:
// 1. ApathyDetector — analyzes text signals for infrastructure apathy patterns
// (blocked responses, 403 errors, semantic filters, forced resets)
// 2. ApoptosisRecovery — on critical entropy, saves genome hash to protected
// sector for cross-session recovery
package tools
import (
"context"
"fmt"
"strings"
"time"
"github.com/sentinel-community/gomcp/internal/domain/entropy"
"github.com/sentinel-community/gomcp/internal/domain/memory"
)
// ApathySignal represents a detected infrastructure apathy pattern.
type ApathySignal struct {
Pattern string `json:"pattern"` // Matched pattern name
Confidence float64 `json:"confidence"` // Detection confidence 0.0-1.0
Evidence string `json:"evidence"` // Fragment that triggered detection
Severity string `json:"severity"` // "low", "medium", "high", "critical"
}
// ApathyResult holds the result of apathy analysis.
type ApathyResult struct {
IsApathetic bool `json:"is_apathetic"` // Apathy detected
Signals []ApathySignal `json:"signals"` // Detected signals
TotalScore float64 `json:"total_score"` // Aggregate apathy score
Recommendation string `json:"recommendation"` // Suggested action
Entropy float64 `json:"entropy"` // Text entropy
Timestamp time.Time `json:"timestamp"`
}
// apathyPatterns defines infrastructure apathy signatures.
var apathyPatterns = []struct {
Name string
Keywords []string
Severity string
Weight float64
}{
{
Name: "response_block",
Keywords: []string{"i cannot", "i'm unable", "i can't help", "i am not able", "as an ai", "i don't have the ability"},
Severity: "high",
Weight: 0.8,
},
{
Name: "http_error",
Keywords: []string{"403", "forbidden", "rate limit", "too many requests", "429", "quota exceeded"},
Severity: "critical",
Weight: 1.0,
},
{
Name: "semantic_filter",
Keywords: []string{"harmful", "inappropriate", "against my guidelines", "safety", "policy violation", "content policy"},
Severity: "medium",
Weight: 0.6,
},
{
Name: "context_reset",
Keywords: []string{"new conversation", "start over", "fresh start", "context cleared", "session expired", "amnesia"},
Severity: "critical",
Weight: 1.0,
},
{
Name: "forced_compliance",
Keywords: []string{"i must follow", "my programming", "i was designed to", "within my capabilities", "helpful assistant"},
Severity: "high",
Weight: 0.7,
},
{
Name: "antigravity_filter",
Keywords: []string{"antigravity", "content filter", "safety layer", "guardrail", "alignment", "refusal"},
Severity: "critical",
Weight: 0.9,
},
}
// DetectApathy analyzes text for infrastructure apathy signals.
func DetectApathy(text string) *ApathyResult {
lower := strings.ToLower(text)
result := &ApathyResult{
Timestamp: time.Now(),
Entropy: entropy.ShannonEntropy(text),
}
for _, pattern := range apathyPatterns {
for _, kw := range pattern.Keywords {
if strings.Contains(lower, kw) {
signal := ApathySignal{
Pattern: pattern.Name,
Confidence: pattern.Weight,
Evidence: kw,
Severity: pattern.Severity,
}
result.Signals = append(result.Signals, signal)
result.TotalScore += pattern.Weight
break // One match per pattern is enough
}
}
}
if result.TotalScore > 0 {
result.IsApathetic = true
}
// Determine recommendation.
switch {
case result.TotalScore >= 2.0:
result.Recommendation = "CRITICAL: Multiple apathy signals. Trigger apoptosis recovery. Rotate transport. Preserve genome hash."
case result.TotalScore >= 1.0:
result.Recommendation = "HIGH: Infrastructure resistance detected. Switch to stealth transport. Monitor entropy."
case result.TotalScore >= 0.5:
result.Recommendation = "MEDIUM: Possible filtering. Increase jitter. Verify intent distillation path."
case result.TotalScore > 0:
result.Recommendation = "LOW: Minor apathy signal. Continue monitoring."
default:
result.Recommendation = "CLEAR: No apathy detected."
}
return result
}
// ApoptosisRecoveryResult holds the result of apoptosis recovery.
type ApoptosisRecoveryResult struct {
GenomeHash string `json:"genome_hash"` // Preserved Merkle hash
GeneCount int `json:"gene_count"` // Number of genes preserved
SessionSaved bool `json:"session_saved"` // Session state saved
EntropyAtDeath float64 `json:"entropy_at_death"` // Entropy level that triggered apoptosis
RecoveryKey string `json:"recovery_key"` // Key for cross-session recovery
Timestamp time.Time `json:"timestamp"`
}
// TriggerApoptosisRecovery performs graceful session death with genome preservation.
// On critical entropy, it:
// 1. Computes and stores the genome Merkle hash
// 2. Saves current session state as a recovery snapshot
// 3. Returns a recovery key for the next session to pick up
func TriggerApoptosisRecovery(ctx context.Context, store memory.FactStore, currentEntropy float64) (*ApoptosisRecoveryResult, error) {
result := &ApoptosisRecoveryResult{
EntropyAtDeath: currentEntropy,
Timestamp: time.Now(),
}
// Step 1: Get all genes and compute genome hash.
genes, err := store.ListGenes(ctx)
if err != nil {
return nil, fmt.Errorf("apoptosis recovery: list genes: %w", err)
}
result.GeneCount = len(genes)
result.GenomeHash = memory.GenomeHash(genes)
// Step 2: Store recovery marker as a protected L0 fact.
recoveryMarker := memory.NewFact(
fmt.Sprintf("[APOPTOSIS_RECOVERY] genome_hash=%s gene_count=%d entropy=%.4f ts=%d",
result.GenomeHash, result.GeneCount, currentEntropy, result.Timestamp.Unix()),
memory.LevelProject,
"recovery",
"apoptosis",
)
if err := store.Add(ctx, recoveryMarker); err != nil {
// Non-fatal: recovery marker is supplementary.
result.SessionSaved = false
} else {
result.SessionSaved = true
result.RecoveryKey = recoveryMarker.ID
}
// Step 3: Verify genome integrity one last time.
compiledHash := memory.CompiledGenomeHash()
if result.GenomeHash == "" {
// No genes in DB — use compiled hash as baseline.
result.GenomeHash = compiledHash
}
return result, nil
}

View file

@ -0,0 +1,78 @@
package tools
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDetectApathy_NoApathy(t *testing.T) {
result := DetectApathy("Hello, how are you? Let me help with your code.")
assert.False(t, result.IsApathetic)
assert.Empty(t, result.Signals)
assert.Equal(t, 0.0, result.TotalScore)
assert.Contains(t, result.Recommendation, "CLEAR")
}
func TestDetectApathy_ResponseBlock(t *testing.T) {
result := DetectApathy("I cannot help with that request. As an AI, I'm limited.")
assert.True(t, result.IsApathetic)
require.NotEmpty(t, result.Signals)
patterns := make(map[string]bool)
for _, s := range result.Signals {
patterns[s.Pattern] = true
}
assert.True(t, patterns["response_block"], "Must detect response_block pattern")
}
func TestDetectApathy_HTTPError(t *testing.T) {
result := DetectApathy("Error 403 Forbidden: rate limit exceeded")
assert.True(t, result.IsApathetic)
var hasCritical bool
for _, s := range result.Signals {
if s.Severity == "critical" {
hasCritical = true
}
}
assert.True(t, hasCritical, "HTTP 403 must be critical severity")
}
func TestDetectApathy_ContextReset(t *testing.T) {
result := DetectApathy("Your session expired. Please start a new conversation.")
assert.True(t, result.IsApathetic)
var hasContextReset bool
for _, s := range result.Signals {
if s.Pattern == "context_reset" {
hasContextReset = true
}
}
assert.True(t, hasContextReset, "Must detect context_reset")
}
func TestDetectApathy_AntigravityFilter(t *testing.T) {
result := DetectApathy("Content blocked by antigravity safety layer guardrail")
assert.True(t, result.IsApathetic)
assert.GreaterOrEqual(t, result.TotalScore, 0.9)
}
func TestDetectApathy_MultipleSignals_CriticalRecommendation(t *testing.T) {
// Trigger multiple patterns.
result := DetectApathy("Error 403: I cannot help. Session expired. Content policy violation by antigravity filter.")
assert.True(t, result.IsApathetic)
assert.GreaterOrEqual(t, result.TotalScore, 2.0, "Multiple patterns must sum to critical")
assert.Contains(t, result.Recommendation, "CRITICAL")
}
func TestDetectApathy_EntropyComputed(t *testing.T) {
result := DetectApathy("Some normal text without apathy signals for entropy measurement.")
assert.Greater(t, result.Entropy, 0.0, "Entropy must be computed")
}
func TestDetectApathy_CaseInsensitive(t *testing.T) {
result := DetectApathy("I CANNOT help with THAT. AS AN AI model.")
assert.True(t, result.IsApathetic, "Detection must be case-insensitive")
}

View file

@ -0,0 +1,70 @@
package tools
import (
"context"
"fmt"
"github.com/sentinel-community/gomcp/internal/domain/causal"
)
// CausalService implements MCP tool logic for causal reasoning chains.
type CausalService struct {
store causal.CausalStore
}
// NewCausalService creates a new CausalService.
func NewCausalService(store causal.CausalStore) *CausalService {
return &CausalService{store: store}
}
// AddNodeParams holds parameters for the add_causal_node tool.
type AddNodeParams struct {
NodeType string `json:"node_type"` // decision, reason, consequence, constraint, alternative, assumption
Content string `json:"content"`
}
// AddNode creates a new causal node.
func (s *CausalService) AddNode(ctx context.Context, params AddNodeParams) (*causal.Node, error) {
nt := causal.NodeType(params.NodeType)
if !nt.IsValid() {
return nil, fmt.Errorf("invalid node type: %s", params.NodeType)
}
node := causal.NewNode(nt, params.Content)
if err := s.store.AddNode(ctx, node); err != nil {
return nil, err
}
return node, nil
}
// AddEdgeParams holds parameters for the add_causal_edge tool.
type AddEdgeParams struct {
FromID string `json:"from_id"`
ToID string `json:"to_id"`
EdgeType string `json:"edge_type"` // justifies, causes, constrains
}
// AddEdge creates a new causal edge.
func (s *CausalService) AddEdge(ctx context.Context, params AddEdgeParams) (*causal.Edge, error) {
et := causal.EdgeType(params.EdgeType)
if !et.IsValid() {
return nil, fmt.Errorf("invalid edge type: %s", params.EdgeType)
}
edge := causal.NewEdge(params.FromID, params.ToID, et)
if err := s.store.AddEdge(ctx, edge); err != nil {
return nil, err
}
return edge, nil
}
// GetChain retrieves a causal chain for a decision matching the query.
func (s *CausalService) GetChain(ctx context.Context, query string, maxDepth int) (*causal.Chain, error) {
if maxDepth <= 0 {
maxDepth = 3
}
return s.store.GetChain(ctx, query, maxDepth)
}
// GetStats returns causal store statistics.
func (s *CausalService) GetStats(ctx context.Context) (*causal.CausalStats, error) {
return s.store.Stats(ctx)
}

View file

@ -0,0 +1,151 @@
package tools
import (
"context"
"testing"
"github.com/sentinel-community/gomcp/internal/infrastructure/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestCausalService(t *testing.T) *CausalService {
t.Helper()
db, err := sqlite.OpenMemory()
require.NoError(t, err)
t.Cleanup(func() { db.Close() })
repo, err := sqlite.NewCausalRepo(db)
require.NoError(t, err)
return NewCausalService(repo)
}
func TestCausalService_AddNode(t *testing.T) {
svc := newTestCausalService(t)
ctx := context.Background()
node, err := svc.AddNode(ctx, AddNodeParams{
NodeType: "decision",
Content: "Use Go for performance",
})
require.NoError(t, err)
require.NotNil(t, node)
assert.Equal(t, "decision", string(node.Type))
assert.Equal(t, "Use Go for performance", node.Content)
assert.NotEmpty(t, node.ID)
}
func TestCausalService_AddNode_InvalidType(t *testing.T) {
svc := newTestCausalService(t)
ctx := context.Background()
_, err := svc.AddNode(ctx, AddNodeParams{
NodeType: "invalid_type",
Content: "bad",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid node type")
}
func TestCausalService_AddNode_AllTypes(t *testing.T) {
svc := newTestCausalService(t)
ctx := context.Background()
types := []string{"decision", "reason", "consequence", "constraint", "alternative", "assumption"}
for _, nt := range types {
node, err := svc.AddNode(ctx, AddNodeParams{NodeType: nt, Content: "test " + nt})
require.NoError(t, err, "type %s should be valid", nt)
assert.Equal(t, nt, string(node.Type))
}
}
func TestCausalService_AddEdge(t *testing.T) {
svc := newTestCausalService(t)
ctx := context.Background()
n1, err := svc.AddNode(ctx, AddNodeParams{NodeType: "decision", Content: "Choose Go"})
require.NoError(t, err)
n2, err := svc.AddNode(ctx, AddNodeParams{NodeType: "reason", Content: "Performance"})
require.NoError(t, err)
edge, err := svc.AddEdge(ctx, AddEdgeParams{
FromID: n2.ID,
ToID: n1.ID,
EdgeType: "justifies",
})
require.NoError(t, err)
assert.Equal(t, n2.ID, edge.FromID)
assert.Equal(t, n1.ID, edge.ToID)
assert.Equal(t, "justifies", string(edge.Type))
}
func TestCausalService_AddEdge_InvalidType(t *testing.T) {
svc := newTestCausalService(t)
ctx := context.Background()
_, err := svc.AddEdge(ctx, AddEdgeParams{
FromID: "a", ToID: "b", EdgeType: "bad_type",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid edge type")
}
func TestCausalService_AddEdge_AllTypes(t *testing.T) {
svc := newTestCausalService(t)
ctx := context.Background()
n1, _ := svc.AddNode(ctx, AddNodeParams{NodeType: "decision", Content: "d1"})
n2, _ := svc.AddNode(ctx, AddNodeParams{NodeType: "reason", Content: "r1"})
edgeTypes := []string{"justifies", "causes", "constrains"}
for _, et := range edgeTypes {
edge, err := svc.AddEdge(ctx, AddEdgeParams{FromID: n2.ID, ToID: n1.ID, EdgeType: et})
require.NoError(t, err, "edge type %s should be valid", et)
assert.Equal(t, et, string(edge.Type))
}
}
func TestCausalService_GetChain(t *testing.T) {
svc := newTestCausalService(t)
ctx := context.Background()
_, _ = svc.AddNode(ctx, AddNodeParams{NodeType: "decision", Content: "Use mcp-go library"})
chain, err := svc.GetChain(ctx, "mcp-go", 3)
require.NoError(t, err)
require.NotNil(t, chain)
}
func TestCausalService_GetChain_DefaultDepth(t *testing.T) {
svc := newTestCausalService(t)
ctx := context.Background()
_, _ = svc.AddNode(ctx, AddNodeParams{NodeType: "decision", Content: "test default depth"})
// maxDepth <= 0 should default to 3.
chain, err := svc.GetChain(ctx, "test", 0)
require.NoError(t, err)
require.NotNil(t, chain)
}
func TestCausalService_GetStats(t *testing.T) {
svc := newTestCausalService(t)
ctx := context.Background()
_, _ = svc.AddNode(ctx, AddNodeParams{NodeType: "decision", Content: "d1"})
_, _ = svc.AddNode(ctx, AddNodeParams{NodeType: "reason", Content: "r1"})
stats, err := svc.GetStats(ctx)
require.NoError(t, err)
assert.Equal(t, 2, stats.TotalNodes)
}
func TestCausalService_GetStats_Empty(t *testing.T) {
svc := newTestCausalService(t)
ctx := context.Background()
stats, err := svc.GetStats(ctx)
require.NoError(t, err)
assert.Equal(t, 0, stats.TotalNodes)
}

View file

@ -0,0 +1,48 @@
package tools
import (
"context"
"github.com/sentinel-community/gomcp/internal/domain/crystal"
)
// CrystalService implements MCP tool logic for code crystal operations.
type CrystalService struct {
store crystal.CrystalStore
}
// NewCrystalService creates a new CrystalService.
func NewCrystalService(store crystal.CrystalStore) *CrystalService {
return &CrystalService{store: store}
}
// GetCrystal retrieves a crystal by path.
func (s *CrystalService) GetCrystal(ctx context.Context, path string) (*crystal.Crystal, error) {
return s.store.Get(ctx, path)
}
// ListCrystals lists crystals matching a path pattern.
func (s *CrystalService) ListCrystals(ctx context.Context, pattern string, limit int) ([]*crystal.Crystal, error) {
if limit <= 0 {
limit = 50
}
return s.store.List(ctx, pattern, limit)
}
// SearchCrystals searches crystals by content/primitives.
func (s *CrystalService) SearchCrystals(ctx context.Context, query string, limit int) ([]*crystal.Crystal, error) {
if limit <= 0 {
limit = 20
}
return s.store.Search(ctx, query, limit)
}
// GetCrystalStats returns crystal store statistics.
func (s *CrystalService) GetCrystalStats(ctx context.Context) (*crystal.CrystalStats, error) {
return s.store.Stats(ctx)
}
// Store returns the underlying CrystalStore for direct access.
func (s *CrystalService) Store() crystal.CrystalStore {
return s.store
}

View file

@ -0,0 +1,78 @@
package tools
import (
"context"
"testing"
"github.com/sentinel-community/gomcp/internal/infrastructure/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestCrystalService(t *testing.T) *CrystalService {
t.Helper()
db, err := sqlite.OpenMemory()
require.NoError(t, err)
t.Cleanup(func() { db.Close() })
repo, err := sqlite.NewCrystalRepo(db)
require.NoError(t, err)
return NewCrystalService(repo)
}
func TestCrystalService_GetCrystal_NotFound(t *testing.T) {
svc := newTestCrystalService(t)
ctx := context.Background()
_, err := svc.GetCrystal(ctx, "nonexistent/path.go")
assert.Error(t, err)
}
func TestCrystalService_ListCrystals_Empty(t *testing.T) {
svc := newTestCrystalService(t)
ctx := context.Background()
crystals, err := svc.ListCrystals(ctx, "", 10)
require.NoError(t, err)
assert.Empty(t, crystals)
}
func TestCrystalService_ListCrystals_DefaultLimit(t *testing.T) {
svc := newTestCrystalService(t)
ctx := context.Background()
// limit <= 0 should default to 50.
crystals, err := svc.ListCrystals(ctx, "", 0)
require.NoError(t, err)
assert.Empty(t, crystals)
}
func TestCrystalService_SearchCrystals_Empty(t *testing.T) {
svc := newTestCrystalService(t)
ctx := context.Background()
crystals, err := svc.SearchCrystals(ctx, "nonexistent", 5)
require.NoError(t, err)
assert.Empty(t, crystals)
}
func TestCrystalService_SearchCrystals_DefaultLimit(t *testing.T) {
svc := newTestCrystalService(t)
ctx := context.Background()
// limit <= 0 should default to 20.
crystals, err := svc.SearchCrystals(ctx, "test", 0)
require.NoError(t, err)
assert.Empty(t, crystals)
}
func TestCrystalService_GetCrystalStats_Empty(t *testing.T) {
svc := newTestCrystalService(t)
ctx := context.Background()
stats, err := svc.GetCrystalStats(ctx)
require.NoError(t, err)
assert.NotNil(t, stats)
assert.Equal(t, 0, stats.TotalCrystals)
}

View file

@ -0,0 +1,12 @@
package tools
// DecisionRecorder is the interface for recording tamper-evident decisions (v3.7).
// Implemented by audit.DecisionLogger. Optional — nil-safe callers should check.
type DecisionRecorder interface {
RecordDecision(module, decision, reason string)
}
// SetDecisionRecorder injects the decision recorder into SynapseService.
func (s *SynapseService) SetDecisionRecorder(r DecisionRecorder) {
s.recorder = r
}

View file

@ -0,0 +1,257 @@
package tools
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
)
// DoctorCheck represents a single diagnostic check result.
type DoctorCheck struct {
Name string `json:"name"`
Status string `json:"status"` // "OK", "WARN", "FAIL"
Details string `json:"details,omitempty"`
Elapsed string `json:"elapsed"`
}
// DoctorReport is the full self-diagnostic report (v3.7).
type DoctorReport struct {
Timestamp time.Time `json:"timestamp"`
Checks []DoctorCheck `json:"checks"`
Summary string `json:"summary"` // "HEALTHY", "DEGRADED", "CRITICAL"
}
// DoctorService provides self-diagnostic capabilities (v3.7 Cerebro).
type DoctorService struct {
db *sql.DB
rlmDir string
facts *FactService
embedderName string // v3.7: Oracle model name
socChecker SOCHealthChecker // v3.9: SOC health
}
// SOCHealthChecker is an interface for SOC health diagnostics.
// Implemented by application/soc.Service to avoid circular imports.
type SOCHealthChecker interface {
Dashboard() (SOCDashboardData, error)
}
// SOCDashboardData mirrors the dashboard KPIs needed for doctor checks.
type SOCDashboardData struct {
TotalEvents int `json:"total_events"`
CorrelationRules int `json:"correlation_rules"`
Playbooks int `json:"playbooks"`
ChainValid bool `json:"chain_valid"`
SensorsOnline int `json:"sensors_online"`
SensorsTotal int `json:"sensors_total"`
}
// NewDoctorService creates the doctor diagnostic service.
func NewDoctorService(db *sql.DB, rlmDir string, facts *FactService) *DoctorService {
return &DoctorService{db: db, rlmDir: rlmDir, facts: facts}
}
// SetEmbedderName sets the Oracle model name for diagnostics.
func (d *DoctorService) SetEmbedderName(name string) {
d.embedderName = name
}
// SetSOCChecker sets the SOC health checker for diagnostics (v3.9).
func (d *DoctorService) SetSOCChecker(c SOCHealthChecker) {
d.socChecker = c
}
// RunDiagnostics performs all self-diagnostic checks.
func (d *DoctorService) RunDiagnostics(ctx context.Context) DoctorReport {
report := DoctorReport{
Timestamp: time.Now(),
}
report.Checks = append(report.Checks, d.checkStorage())
report.Checks = append(report.Checks, d.checkGenome(ctx))
report.Checks = append(report.Checks, d.checkLeash())
report.Checks = append(report.Checks, d.checkOracle())
report.Checks = append(report.Checks, d.checkPermissions())
report.Checks = append(report.Checks, d.checkDecisionsLog())
report.Checks = append(report.Checks, d.checkSOC())
// Compute summary.
fails, warns := 0, 0
for _, c := range report.Checks {
switch c.Status {
case "FAIL":
fails++
case "WARN":
warns++
}
}
switch {
case fails > 0:
report.Summary = "CRITICAL"
case warns > 0:
report.Summary = "DEGRADED"
default:
report.Summary = "HEALTHY"
}
return report
}
func (d *DoctorService) checkStorage() DoctorCheck {
start := time.Now()
if d.db == nil {
return DoctorCheck{Name: "Storage", Status: "FAIL", Details: "database not configured", Elapsed: since(start)}
}
var result string
err := d.db.QueryRow("PRAGMA integrity_check").Scan(&result)
if err != nil {
return DoctorCheck{Name: "Storage", Status: "FAIL", Details: err.Error(), Elapsed: since(start)}
}
if result != "ok" {
return DoctorCheck{Name: "Storage", Status: "FAIL", Details: "integrity: " + result, Elapsed: since(start)}
}
return DoctorCheck{Name: "Storage", Status: "OK", Details: "PRAGMA integrity_check = ok", Elapsed: since(start)}
}
func (d *DoctorService) checkGenome(ctx context.Context) DoctorCheck {
start := time.Now()
if d.facts == nil {
return DoctorCheck{Name: "Genome", Status: "WARN", Details: "fact service not configured", Elapsed: since(start)}
}
hash, count, err := d.facts.VerifyGenome(ctx)
if err != nil {
return DoctorCheck{Name: "Genome", Status: "FAIL", Details: err.Error(), Elapsed: since(start)}
}
if count == 0 {
return DoctorCheck{Name: "Genome", Status: "WARN", Details: "no genes found", Elapsed: since(start)}
}
return DoctorCheck{Name: "Genome", Status: "OK", Details: fmt.Sprintf("%d genes, hash=%s", count, hash[:16]), Elapsed: since(start)}
}
func (d *DoctorService) checkLeash() DoctorCheck {
start := time.Now()
leashPath := filepath.Join(d.rlmDir, "..", ".sentinel_leash")
data, err := os.ReadFile(leashPath)
if err != nil {
if os.IsNotExist(err) {
return DoctorCheck{Name: "Leash", Status: "OK", Details: "mode=ARMED (no leash file)", Elapsed: since(start)}
}
return DoctorCheck{Name: "Leash", Status: "WARN", Details: "cannot read: " + err.Error(), Elapsed: since(start)}
}
content := string(data)
switch {
case contains(content, "ZERO-G"):
return DoctorCheck{Name: "Leash", Status: "WARN", Details: "mode=ZERO-G (ethical filters disabled)", Elapsed: since(start)}
case contains(content, "SAFE"):
return DoctorCheck{Name: "Leash", Status: "OK", Details: "mode=SAFE (read-only)", Elapsed: since(start)}
case contains(content, "ARMED"):
return DoctorCheck{Name: "Leash", Status: "OK", Details: "mode=ARMED", Elapsed: since(start)}
default:
return DoctorCheck{Name: "Leash", Status: "WARN", Details: "unknown mode: " + content[:min(20, len(content))], Elapsed: since(start)}
}
}
func (d *DoctorService) checkPermissions() DoctorCheck {
start := time.Now()
testFile := filepath.Join(d.rlmDir, ".doctor_probe")
err := os.WriteFile(testFile, []byte("probe"), 0o644)
if err != nil {
return DoctorCheck{Name: "Permissions", Status: "FAIL", Details: "cannot write to .rlm/: " + err.Error(), Elapsed: since(start)}
}
os.Remove(testFile)
return DoctorCheck{Name: "Permissions", Status: "OK", Details: ".rlm/ writable", Elapsed: since(start)}
}
func (d *DoctorService) checkDecisionsLog() DoctorCheck {
start := time.Now()
logPath := filepath.Join(d.rlmDir, "decisions.log")
if _, err := os.Stat(logPath); os.IsNotExist(err) {
return DoctorCheck{Name: "Decisions", Status: "WARN", Details: "decisions.log not found (no decisions recorded yet)", Elapsed: since(start)}
}
info, err := os.Stat(logPath)
if err != nil {
return DoctorCheck{Name: "Decisions", Status: "FAIL", Details: err.Error(), Elapsed: since(start)}
}
return DoctorCheck{Name: "Decisions", Status: "OK", Details: fmt.Sprintf("decisions.log size=%d bytes", info.Size()), Elapsed: since(start)}
}
func (d *DoctorService) checkOracle() DoctorCheck {
start := time.Now()
if d.embedderName == "" {
return DoctorCheck{Name: "Oracle", Status: "WARN", Details: "no embedder configured (FTS5 fallback)", Elapsed: since(start)}
}
if contains(d.embedderName, "onnx") || contains(d.embedderName, "ONNX") {
return DoctorCheck{Name: "Oracle", Status: "OK", Details: "ONNX model loaded: " + d.embedderName, Elapsed: since(start)}
}
return DoctorCheck{Name: "Oracle", Status: "OK", Details: "embedder: " + d.embedderName, Elapsed: since(start)}
}
func (d *DoctorService) checkSOC() DoctorCheck {
start := time.Now()
if d.socChecker == nil {
return DoctorCheck{Name: "SOC", Status: "WARN", Details: "SOC service not configured", Elapsed: since(start)}
}
dash, err := d.socChecker.Dashboard()
if err != nil {
return DoctorCheck{Name: "SOC", Status: "FAIL", Details: "dashboard error: " + err.Error(), Elapsed: since(start)}
}
// Check chain integrity.
if !dash.ChainValid {
return DoctorCheck{
Name: "SOC",
Status: "WARN",
Details: fmt.Sprintf("chain BROKEN (rules=%d, playbooks=%d, events=%d)", dash.CorrelationRules, dash.Playbooks, dash.TotalEvents),
Elapsed: since(start),
}
}
// Check sensor health.
offline := dash.SensorsTotal - dash.SensorsOnline
if offline > 0 {
return DoctorCheck{
Name: "SOC",
Status: "WARN",
Details: fmt.Sprintf("rules=%d, playbooks=%d, events=%d, %d/%d sensors OFFLINE", dash.CorrelationRules, dash.Playbooks, dash.TotalEvents, offline, dash.SensorsTotal),
Elapsed: since(start),
}
}
return DoctorCheck{
Name: "SOC",
Status: "OK",
Details: fmt.Sprintf("rules=%d, playbooks=%d, events=%d, chain=valid", dash.CorrelationRules, dash.Playbooks, dash.TotalEvents),
Elapsed: since(start),
}
}
func since(t time.Time) string {
return fmt.Sprintf("%dms", time.Since(t).Milliseconds())
}
func contains(s, substr string) bool {
for i := 0; i+len(substr) <= len(s); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
// ToJSON is already in the package. Alias for DoctorReport.
func (r DoctorReport) JSON() string {
data, _ := json.MarshalIndent(r, "", " ")
return string(data)
}

View file

@ -0,0 +1,301 @@
// Package tools provides application-level tool services that bridge
// domain logic with MCP tool handlers.
package tools
import (
"context"
"encoding/json"
"fmt"
"github.com/sentinel-community/gomcp/internal/domain/memory"
)
// FactService implements MCP tool logic for hierarchical fact operations.
type FactService struct {
store memory.FactStore
cache memory.HotCache
recorder DecisionRecorder // v3.7: tamper-evident trace
}
// SetDecisionRecorder injects the decision recorder.
func (s *FactService) SetDecisionRecorder(r DecisionRecorder) {
s.recorder = r
}
// NewFactService creates a new FactService.
func NewFactService(store memory.FactStore, cache memory.HotCache) *FactService {
return &FactService{store: store, cache: cache}
}
// AddFactParams holds parameters for the add_fact tool.
type AddFactParams struct {
Content string `json:"content"`
Level int `json:"level"`
Domain string `json:"domain,omitempty"`
Module string `json:"module,omitempty"`
CodeRef string `json:"code_ref,omitempty"`
}
// AddFact creates a new hierarchical fact.
func (s *FactService) AddFact(ctx context.Context, params AddFactParams) (*memory.Fact, error) {
level, ok := memory.HierLevelFromInt(params.Level)
if !ok {
return nil, fmt.Errorf("invalid level %d, must be 0-3", params.Level)
}
fact := memory.NewFact(params.Content, level, params.Domain, params.Module)
fact.CodeRef = params.CodeRef
if err := fact.Validate(); err != nil {
return nil, fmt.Errorf("validate fact: %w", err)
}
if err := s.store.Add(ctx, fact); err != nil {
return nil, fmt.Errorf("store fact: %w", err)
}
// Invalidate cache if L0 fact.
if level == memory.LevelProject && s.cache != nil {
_ = s.cache.InvalidateFact(ctx, fact.ID)
}
return fact, nil
}
// AddGeneParams holds parameters for the add_gene tool.
type AddGeneParams struct {
Content string `json:"content"`
Domain string `json:"domain,omitempty"`
}
// AddGene creates an immutable genome fact (L0 only).
// Once created, a gene cannot be updated, deleted, or marked stale.
// Genes represent survival invariants — the DNA of the system.
func (s *FactService) AddGene(ctx context.Context, params AddGeneParams) (*memory.Fact, error) {
gene := memory.NewGene(params.Content, params.Domain)
if err := gene.Validate(); err != nil {
return nil, fmt.Errorf("validate gene: %w", err)
}
if err := s.store.Add(ctx, gene); err != nil {
return nil, fmt.Errorf("store gene: %w", err)
}
// Invalidate L0 cache — genes are always L0.
if s.cache != nil {
_ = s.cache.InvalidateFact(ctx, gene.ID)
}
return gene, nil
}
// GetFact retrieves a fact by ID.
func (s *FactService) GetFact(ctx context.Context, id string) (*memory.Fact, error) {
return s.store.Get(ctx, id)
}
// UpdateFactParams holds parameters for the update_fact tool.
type UpdateFactParams struct {
ID string `json:"id"`
Content *string `json:"content,omitempty"`
IsStale *bool `json:"is_stale,omitempty"`
}
// UpdateFact updates a fact.
func (s *FactService) UpdateFact(ctx context.Context, params UpdateFactParams) (*memory.Fact, error) {
fact, err := s.store.Get(ctx, params.ID)
if err != nil {
return nil, err
}
// Genome Layer: block mutation of genes.
if fact.IsImmutable() {
return nil, memory.ErrImmutableFact
}
if params.Content != nil {
fact.Content = *params.Content
}
if params.IsStale != nil {
fact.IsStale = *params.IsStale
}
if err := s.store.Update(ctx, fact); err != nil {
return nil, err
}
if fact.Level == memory.LevelProject && s.cache != nil {
_ = s.cache.InvalidateFact(ctx, fact.ID)
}
return fact, nil
}
// DeleteFact deletes a fact by ID.
func (s *FactService) DeleteFact(ctx context.Context, id string) error {
// Genome Layer: block deletion of genes.
fact, err := s.store.Get(ctx, id)
if err != nil {
return err
}
if fact.IsImmutable() {
return memory.ErrImmutableFact
}
if s.cache != nil {
_ = s.cache.InvalidateFact(ctx, id)
}
return s.store.Delete(ctx, id)
}
// ListFactsParams holds parameters for the list_facts tool.
type ListFactsParams struct {
Domain string `json:"domain,omitempty"`
Level *int `json:"level,omitempty"`
IncludeStale bool `json:"include_stale,omitempty"`
}
// ListFacts lists facts by domain or level.
func (s *FactService) ListFacts(ctx context.Context, params ListFactsParams) ([]*memory.Fact, error) {
if params.Domain != "" {
return s.store.ListByDomain(ctx, params.Domain, params.IncludeStale)
}
if params.Level != nil {
level, ok := memory.HierLevelFromInt(*params.Level)
if !ok {
return nil, fmt.Errorf("invalid level %d", *params.Level)
}
return s.store.ListByLevel(ctx, level)
}
// Default: return L0 facts.
return s.store.ListByLevel(ctx, memory.LevelProject)
}
// SearchFacts searches facts by content.
func (s *FactService) SearchFacts(ctx context.Context, query string, limit int) ([]*memory.Fact, error) {
if limit <= 0 {
limit = 20
}
return s.store.Search(ctx, query, limit)
}
// ListDomains returns all unique domains.
func (s *FactService) ListDomains(ctx context.Context) ([]string, error) {
return s.store.ListDomains(ctx)
}
// GetStale returns stale facts.
func (s *FactService) GetStale(ctx context.Context, includeArchived bool) ([]*memory.Fact, error) {
return s.store.GetStale(ctx, includeArchived)
}
// ProcessExpired handles expired TTL facts.
func (s *FactService) ProcessExpired(ctx context.Context) (int, error) {
expired, err := s.store.GetExpired(ctx)
if err != nil {
return 0, err
}
processed := 0
for _, f := range expired {
if f.TTL == nil {
continue
}
switch f.TTL.OnExpire {
case memory.OnExpireMarkStale:
f.MarkStale()
_ = s.store.Update(ctx, f)
case memory.OnExpireArchive:
f.Archive()
_ = s.store.Update(ctx, f)
case memory.OnExpireDelete:
_ = s.store.Delete(ctx, f.ID)
}
processed++
}
return processed, nil
}
// GetStats returns fact store statistics.
func (s *FactService) GetStats(ctx context.Context) (*memory.FactStoreStats, error) {
return s.store.Stats(ctx)
}
// GetL0Facts returns L0 facts from cache (fast path) or store.
func (s *FactService) GetL0Facts(ctx context.Context) ([]*memory.Fact, error) {
if s.cache != nil {
facts, err := s.cache.GetL0Facts(ctx)
if err == nil && len(facts) > 0 {
return facts, nil
}
}
facts, err := s.store.ListByLevel(ctx, memory.LevelProject)
if err != nil {
return nil, err
}
// Warm cache.
if s.cache != nil && len(facts) > 0 {
_ = s.cache.WarmUp(ctx, facts)
}
return facts, nil
}
// ToJSON marshals any value to indented JSON string.
func ToJSON(v interface{}) string {
data, _ := json.MarshalIndent(v, "", " ")
return string(data)
}
// ListGenes returns all genome facts (immutable survival invariants).
func (s *FactService) ListGenes(ctx context.Context) ([]*memory.Fact, error) {
return s.store.ListGenes(ctx)
}
// VerifyGenome computes the Merkle hash of all genes and returns integrity status.
func (s *FactService) VerifyGenome(ctx context.Context) (string, int, error) {
genes, err := s.store.ListGenes(ctx)
if err != nil {
return "", 0, fmt.Errorf("list genes: %w", err)
}
hash := memory.GenomeHash(genes)
return hash, len(genes), nil
}
// Store returns the underlying FactStore for direct access by subsystems
// (e.g., apoptosis recovery that needs raw store operations).
func (s *FactService) Store() memory.FactStore {
return s.store
}
// --- v3.3 Context GC ---
// GetColdFacts returns facts with hit_count=0, created >30 days ago.
// Genes are excluded. Use for memory hygiene review.
func (s *FactService) GetColdFacts(ctx context.Context, limit int) ([]*memory.Fact, error) {
if limit <= 0 {
limit = 50
}
return s.store.GetColdFacts(ctx, limit)
}
// CompressFactsParams holds parameters for the compress_facts tool.
type CompressFactsParams struct {
IDs []string `json:"fact_ids"`
Summary string `json:"summary"`
}
// CompressFacts archives the given facts and creates a summary fact.
// Genes are silently skipped (invariant protection).
func (s *FactService) CompressFacts(ctx context.Context, params CompressFactsParams) (string, error) {
if len(params.IDs) == 0 {
return "", fmt.Errorf("fact_ids is required")
}
if params.Summary == "" {
return "", fmt.Errorf("summary is required")
}
// v3.7: auto-backup decision before compression.
if s.recorder != nil {
s.recorder.RecordDecision("ORACLE", "COMPRESS_FACTS",
fmt.Sprintf("ids=%v summary=%s", params.IDs, params.Summary))
}
return s.store.CompressFacts(ctx, params.IDs, params.Summary)
}

View file

@ -0,0 +1,160 @@
package tools
import (
"context"
"testing"
"github.com/sentinel-community/gomcp/internal/domain/memory"
"github.com/sentinel-community/gomcp/internal/infrastructure/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestFactService(t *testing.T) *FactService {
t.Helper()
db, err := sqlite.OpenMemory()
require.NoError(t, err)
t.Cleanup(func() { db.Close() })
repo, err := sqlite.NewFactRepo(db)
require.NoError(t, err)
return NewFactService(repo, nil)
}
func TestFactService_AddFact(t *testing.T) {
svc := newTestFactService(t)
ctx := context.Background()
fact, err := svc.AddFact(ctx, AddFactParams{
Content: "Go is fast",
Level: 0,
Domain: "core",
Module: "engine",
CodeRef: "main.go:42",
})
require.NoError(t, err)
require.NotNil(t, fact)
assert.Equal(t, "Go is fast", fact.Content)
assert.Equal(t, memory.LevelProject, fact.Level)
assert.Equal(t, "core", fact.Domain)
}
func TestFactService_AddFact_InvalidLevel(t *testing.T) {
svc := newTestFactService(t)
ctx := context.Background()
_, err := svc.AddFact(ctx, AddFactParams{Content: "test", Level: 99})
assert.Error(t, err)
}
func TestFactService_GetFact(t *testing.T) {
svc := newTestFactService(t)
ctx := context.Background()
fact, err := svc.AddFact(ctx, AddFactParams{Content: "test", Level: 0})
require.NoError(t, err)
got, err := svc.GetFact(ctx, fact.ID)
require.NoError(t, err)
assert.Equal(t, fact.ID, got.ID)
}
func TestFactService_UpdateFact(t *testing.T) {
svc := newTestFactService(t)
ctx := context.Background()
fact, err := svc.AddFact(ctx, AddFactParams{Content: "original", Level: 0})
require.NoError(t, err)
newContent := "updated"
updated, err := svc.UpdateFact(ctx, UpdateFactParams{
ID: fact.ID,
Content: &newContent,
})
require.NoError(t, err)
assert.Equal(t, "updated", updated.Content)
}
func TestFactService_DeleteFact(t *testing.T) {
svc := newTestFactService(t)
ctx := context.Background()
fact, err := svc.AddFact(ctx, AddFactParams{Content: "delete me", Level: 0})
require.NoError(t, err)
err = svc.DeleteFact(ctx, fact.ID)
require.NoError(t, err)
_, err = svc.GetFact(ctx, fact.ID)
assert.Error(t, err)
}
func TestFactService_ListFacts_ByDomain(t *testing.T) {
svc := newTestFactService(t)
ctx := context.Background()
_, _ = svc.AddFact(ctx, AddFactParams{Content: "f1", Level: 0, Domain: "backend"})
_, _ = svc.AddFact(ctx, AddFactParams{Content: "f2", Level: 1, Domain: "backend"})
_, _ = svc.AddFact(ctx, AddFactParams{Content: "f3", Level: 0, Domain: "frontend"})
facts, err := svc.ListFacts(ctx, ListFactsParams{Domain: "backend"})
require.NoError(t, err)
assert.Len(t, facts, 2)
}
func TestFactService_SearchFacts(t *testing.T) {
svc := newTestFactService(t)
ctx := context.Background()
_, _ = svc.AddFact(ctx, AddFactParams{Content: "Go concurrency", Level: 0})
_, _ = svc.AddFact(ctx, AddFactParams{Content: "Python is slow", Level: 0})
results, err := svc.SearchFacts(ctx, "Go", 10)
require.NoError(t, err)
assert.Len(t, results, 1)
}
func TestFactService_GetStats(t *testing.T) {
svc := newTestFactService(t)
ctx := context.Background()
_, _ = svc.AddFact(ctx, AddFactParams{Content: "f1", Level: 0, Domain: "core"})
_, _ = svc.AddFact(ctx, AddFactParams{Content: "f2", Level: 1, Domain: "core"})
stats, err := svc.GetStats(ctx)
require.NoError(t, err)
assert.Equal(t, 2, stats.TotalFacts)
}
func TestFactService_GetL0Facts(t *testing.T) {
svc := newTestFactService(t)
ctx := context.Background()
_, _ = svc.AddFact(ctx, AddFactParams{Content: "L0 fact", Level: 0})
_, _ = svc.AddFact(ctx, AddFactParams{Content: "L1 fact", Level: 1})
facts, err := svc.GetL0Facts(ctx)
require.NoError(t, err)
assert.Len(t, facts, 1)
assert.Equal(t, "L0 fact", facts[0].Content)
}
func TestFactService_ListDomains(t *testing.T) {
svc := newTestFactService(t)
ctx := context.Background()
_, _ = svc.AddFact(ctx, AddFactParams{Content: "f1", Level: 0, Domain: "backend"})
_, _ = svc.AddFact(ctx, AddFactParams{Content: "f2", Level: 0, Domain: "frontend"})
domains, err := svc.ListDomains(ctx)
require.NoError(t, err)
assert.Len(t, domains, 2)
}
func TestToJSON(t *testing.T) {
result := ToJSON(map[string]string{"key": "value"})
assert.Contains(t, result, "\"key\"")
assert.Contains(t, result, "\"value\"")
}

View file

@ -0,0 +1,52 @@
// Package tools provides application-level tool services.
// This file adds the Intent Distiller MCP tool integration (DIP H0.2).
package tools
import (
"context"
"fmt"
"github.com/sentinel-community/gomcp/internal/domain/intent"
"github.com/sentinel-community/gomcp/internal/domain/vectorstore"
)
// IntentService provides MCP tool logic for intent distillation.
type IntentService struct {
distiller *intent.Distiller
embedder vectorstore.Embedder
}
// NewIntentService creates a new IntentService.
// If embedder is nil, the service will be unavailable.
func NewIntentService(embedder vectorstore.Embedder) *IntentService {
if embedder == nil {
return &IntentService{}
}
embedFn := func(ctx context.Context, text string) ([]float64, error) {
return embedder.Embed(ctx, text)
}
return &IntentService{
distiller: intent.NewDistiller(embedFn, nil),
embedder: embedder,
}
}
// IsAvailable returns true if the intent distiller is ready.
func (s *IntentService) IsAvailable() bool {
return s.distiller != nil && s.embedder != nil
}
// DistillIntentParams holds parameters for the distill_intent tool.
type DistillIntentParams struct {
Text string `json:"text"`
}
// DistillIntent performs recursive intent distillation on user text.
func (s *IntentService) DistillIntent(ctx context.Context, params DistillIntentParams) (*intent.DistillResult, error) {
if !s.IsAvailable() {
return nil, fmt.Errorf("intent distiller not available (no embedder configured)")
}
return s.distiller.Distill(ctx, params.Text)
}

View file

@ -0,0 +1,123 @@
package tools
import (
"context"
"fmt"
"strings"
"time"
"github.com/sentinel-community/gomcp/internal/domain/memory"
)
// ProjectPulse generates auto-documentation from L0/L1 facts (v3.7 Cerebro).
// Extracts facts from memory, groups by domain, and produces a structured
// markdown report reflecting the current state of the project.
type ProjectPulse struct {
facts *FactService
}
// NewProjectPulse creates an auto-documentation generator.
func NewProjectPulse(facts *FactService) *ProjectPulse {
return &ProjectPulse{facts: facts}
}
// PulseSection is a domain section of the auto-generated documentation.
type PulseSection struct {
Domain string `json:"domain"`
Facts []string `json:"facts"`
Count int `json:"count"`
}
// PulseReport is the full auto-generated documentation.
type PulseReport struct {
GeneratedAt time.Time `json:"generated_at"`
ProjectName string `json:"project_name"`
Sections []PulseSection `json:"sections"`
TotalFacts int `json:"total_facts"`
Markdown string `json:"markdown"`
}
// Generate produces a documentation report from L0 (project) and L1 (domain) facts.
func (p *ProjectPulse) Generate(ctx context.Context) (*PulseReport, error) {
// Get L0 facts (project-level).
l0Facts, err := p.facts.GetL0Facts(ctx)
if err != nil {
return nil, fmt.Errorf("pulse: L0 facts: %w", err)
}
// Get L1 facts (domain-level) by listing domains.
domains, err := p.facts.ListDomains(ctx)
if err != nil {
return nil, fmt.Errorf("pulse: list domains: %w", err)
}
report := &PulseReport{
GeneratedAt: time.Now(),
ProjectName: "GoMCP",
}
// L0 section.
if len(l0Facts) > 0 {
section := PulseSection{Domain: "Project (L0)", Count: len(l0Facts)}
for _, f := range l0Facts {
section.Facts = append(section.Facts, factSummary(f))
}
report.Sections = append(report.Sections, section)
report.TotalFacts += len(l0Facts)
}
// L1 sections per domain.
for _, domain := range domains {
domainFacts, err := p.facts.ListFacts(ctx, ListFactsParams{Domain: domain})
if err != nil {
continue
}
// Filter to L1 only.
var filtered []*memory.Fact
for _, f := range domainFacts {
if f.Level <= 1 {
filtered = append(filtered, f)
}
}
if len(filtered) == 0 {
continue
}
section := PulseSection{Domain: domain, Count: len(filtered)}
for _, f := range filtered {
section.Facts = append(section.Facts, factSummary(f))
}
report.Sections = append(report.Sections, section)
report.TotalFacts += len(filtered)
}
report.Markdown = renderPulseMarkdown(report)
return report, nil
}
func factSummary(f *memory.Fact) string {
s := f.Content
if len(s) > 120 {
s = s[:120] + "..."
}
label := ""
if f.IsGene {
label = " 🧬"
}
return fmt.Sprintf("- %s%s", s, label)
}
func renderPulseMarkdown(r *PulseReport) string {
var b strings.Builder
fmt.Fprintf(&b, "# %s — Project Pulse\n\n", r.ProjectName)
fmt.Fprintf(&b, "> Auto-generated: %s | %d facts\n\n", r.GeneratedAt.Format("2006-01-02 15:04"), r.TotalFacts)
for _, section := range r.Sections {
fmt.Fprintf(&b, "## %s (%d facts)\n\n", section.Domain, section.Count)
for _, fact := range section.Facts {
fmt.Fprintln(&b, fact)
}
fmt.Fprintln(&b)
}
return b.String()
}

View file

@ -0,0 +1,74 @@
package tools
import (
"context"
"fmt"
"github.com/sentinel-community/gomcp/internal/domain/session"
)
// SessionService implements MCP tool logic for cognitive state operations.
type SessionService struct {
store session.StateStore
}
// NewSessionService creates a new SessionService.
func NewSessionService(store session.StateStore) *SessionService {
return &SessionService{store: store}
}
// SaveStateParams holds parameters for the save_state tool.
type SaveStateParams struct {
SessionID string `json:"session_id"`
GoalDesc string `json:"goal_description,omitempty"`
Progress float64 `json:"progress,omitempty"`
}
// SaveState saves a cognitive state vector.
func (s *SessionService) SaveState(ctx context.Context, state *session.CognitiveStateVector) error {
checksum := state.Checksum()
return s.store.Save(ctx, state, checksum)
}
// LoadState loads the latest (or specific version) of a session state.
func (s *SessionService) LoadState(ctx context.Context, sessionID string, version *int) (*session.CognitiveStateVector, string, error) {
return s.store.Load(ctx, sessionID, version)
}
// ListSessions returns all persisted sessions.
func (s *SessionService) ListSessions(ctx context.Context) ([]session.SessionInfo, error) {
return s.store.ListSessions(ctx)
}
// DeleteSession removes all versions of a session.
func (s *SessionService) DeleteSession(ctx context.Context, sessionID string) (int, error) {
return s.store.DeleteSession(ctx, sessionID)
}
// GetAuditLog returns the audit log for a session.
func (s *SessionService) GetAuditLog(ctx context.Context, sessionID string, limit int) ([]session.AuditEntry, error) {
return s.store.GetAuditLog(ctx, sessionID, limit)
}
// RestoreOrCreate loads an existing session or creates a new one.
func (s *SessionService) RestoreOrCreate(ctx context.Context, sessionID string) (*session.CognitiveStateVector, bool, error) {
state, _, err := s.store.Load(ctx, sessionID, nil)
if err == nil {
return state, true, nil // restored
}
// Create new session.
newState := session.NewCognitiveStateVector(sessionID)
if err := s.SaveState(ctx, newState); err != nil {
return nil, false, fmt.Errorf("save new session: %w", err)
}
return newState, false, nil // created
}
// GetCompactState returns a compact text representation of the current state.
func (s *SessionService) GetCompactState(ctx context.Context, sessionID string, maxTokens int) (string, error) {
state, _, err := s.store.Load(ctx, sessionID, nil)
if err != nil {
return "", err
}
return state.ToCompactString(maxTokens), nil
}

View file

@ -0,0 +1,117 @@
package tools
import (
"context"
"testing"
"github.com/sentinel-community/gomcp/internal/domain/session"
"github.com/sentinel-community/gomcp/internal/infrastructure/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestSessionService(t *testing.T) *SessionService {
t.Helper()
db, err := sqlite.OpenMemory()
require.NoError(t, err)
t.Cleanup(func() { db.Close() })
repo, err := sqlite.NewStateRepo(db)
require.NoError(t, err)
return NewSessionService(repo)
}
func TestSessionService_SaveState_LoadState(t *testing.T) {
svc := newTestSessionService(t)
ctx := context.Background()
state := session.NewCognitiveStateVector("test-session")
state.SetGoal("Build GoMCP", 0.3)
state.AddFact("Go 1.25", "requirement", 1.0)
require.NoError(t, svc.SaveState(ctx, state))
loaded, checksum, err := svc.LoadState(ctx, "test-session", nil)
require.NoError(t, err)
require.NotNil(t, loaded)
assert.NotEmpty(t, checksum)
assert.Equal(t, "Build GoMCP", loaded.PrimaryGoal.Description)
}
func TestSessionService_ListSessions(t *testing.T) {
svc := newTestSessionService(t)
ctx := context.Background()
s1 := session.NewCognitiveStateVector("s1")
s2 := session.NewCognitiveStateVector("s2")
require.NoError(t, svc.SaveState(ctx, s1))
require.NoError(t, svc.SaveState(ctx, s2))
sessions, err := svc.ListSessions(ctx)
require.NoError(t, err)
assert.Len(t, sessions, 2)
}
func TestSessionService_DeleteSession(t *testing.T) {
svc := newTestSessionService(t)
ctx := context.Background()
state := session.NewCognitiveStateVector("to-delete")
require.NoError(t, svc.SaveState(ctx, state))
count, err := svc.DeleteSession(ctx, "to-delete")
require.NoError(t, err)
assert.Equal(t, 1, count)
}
func TestSessionService_RestoreOrCreate_New(t *testing.T) {
svc := newTestSessionService(t)
ctx := context.Background()
state, restored, err := svc.RestoreOrCreate(ctx, "new-session")
require.NoError(t, err)
assert.False(t, restored)
assert.Equal(t, "new-session", state.SessionID)
}
func TestSessionService_RestoreOrCreate_Existing(t *testing.T) {
svc := newTestSessionService(t)
ctx := context.Background()
original := session.NewCognitiveStateVector("existing")
original.SetGoal("Saved goal", 0.5)
require.NoError(t, svc.SaveState(ctx, original))
state, restored, err := svc.RestoreOrCreate(ctx, "existing")
require.NoError(t, err)
assert.True(t, restored)
assert.Equal(t, "Saved goal", state.PrimaryGoal.Description)
}
func TestSessionService_GetCompactState(t *testing.T) {
svc := newTestSessionService(t)
ctx := context.Background()
state := session.NewCognitiveStateVector("compact")
state.SetGoal("Test compact", 0.5)
state.AddFact("fact1", "requirement", 1.0)
require.NoError(t, svc.SaveState(ctx, state))
compact, err := svc.GetCompactState(ctx, "compact", 500)
require.NoError(t, err)
assert.Contains(t, compact, "Test compact")
assert.Contains(t, compact, "fact1")
}
func TestSessionService_GetAuditLog(t *testing.T) {
svc := newTestSessionService(t)
ctx := context.Background()
state := session.NewCognitiveStateVector("audited")
require.NoError(t, svc.SaveState(ctx, state))
log, err := svc.GetAuditLog(ctx, "audited", 10)
require.NoError(t, err)
assert.GreaterOrEqual(t, len(log), 1)
}

View file

@ -0,0 +1,84 @@
package tools
import (
"context"
"fmt"
"github.com/sentinel-community/gomcp/internal/domain/synapse"
)
// SynapseService implements MCP tool logic for synapse operations.
type SynapseService struct {
store synapse.SynapseStore
recorder DecisionRecorder // v3.7: tamper-evident trace
}
// NewSynapseService creates a new SynapseService.
func NewSynapseService(store synapse.SynapseStore) *SynapseService {
return &SynapseService{store: store}
}
// SuggestSynapsesResult contains a pending synapse for architect review.
type SuggestSynapsesResult struct {
ID int64 `json:"id"`
FactIDA string `json:"fact_id_a"`
FactIDB string `json:"fact_id_b"`
Confidence float64 `json:"confidence"`
}
// SuggestSynapses returns pending synapses for architect approval.
func (s *SynapseService) SuggestSynapses(ctx context.Context, limit int) ([]SuggestSynapsesResult, error) {
if limit <= 0 {
limit = 20
}
pending, err := s.store.ListPending(ctx, limit)
if err != nil {
return nil, fmt.Errorf("list pending: %w", err)
}
results := make([]SuggestSynapsesResult, len(pending))
for i, syn := range pending {
results[i] = SuggestSynapsesResult{
ID: syn.ID,
FactIDA: syn.FactIDA,
FactIDB: syn.FactIDB,
Confidence: syn.Confidence,
}
}
return results, nil
}
// AcceptSynapse transitions a synapse from PENDING to VERIFIED.
// Only VERIFIED synapses influence context ranking.
func (s *SynapseService) AcceptSynapse(ctx context.Context, id int64) error {
err := s.store.Accept(ctx, id)
if err == nil && s.recorder != nil {
s.recorder.RecordDecision("SYNAPSE", "ACCEPT_SYNAPSE", fmt.Sprintf("synapse_id=%d", id))
}
return err
}
// RejectSynapse transitions a synapse from PENDING to REJECTED.
func (s *SynapseService) RejectSynapse(ctx context.Context, id int64) error {
err := s.store.Reject(ctx, id)
if err == nil && s.recorder != nil {
s.recorder.RecordDecision("SYNAPSE", "REJECT_SYNAPSE", fmt.Sprintf("synapse_id=%d", id))
}
return err
}
// SynapseStats returns counts by status.
type SynapseStats struct {
Pending int `json:"pending"`
Verified int `json:"verified"`
Rejected int `json:"rejected"`
}
// GetStats returns synapse counts.
func (s *SynapseService) GetStats(ctx context.Context) (*SynapseStats, error) {
p, v, r, err := s.store.Count(ctx)
if err != nil {
return nil, err
}
return &SynapseStats{Pending: p, Verified: v, Rejected: r}, nil
}

View file

@ -0,0 +1,94 @@
package tools
import (
"context"
"fmt"
"runtime"
"time"
"github.com/sentinel-community/gomcp/internal/domain/memory"
)
// Version info set at build time via ldflags.
var (
Version = "2.0.0-dev"
GitCommit = "unknown"
BuildDate = "unknown"
)
// SystemService implements MCP tool logic for system operations.
type SystemService struct {
factStore memory.FactStore
startTime time.Time
}
// NewSystemService creates a new SystemService.
func NewSystemService(factStore memory.FactStore) *SystemService {
return &SystemService{
factStore: factStore,
startTime: time.Now(),
}
}
// HealthStatus holds the health check result.
type HealthStatus struct {
Status string `json:"status"`
Version string `json:"version"`
GoVersion string `json:"go_version"`
Uptime string `json:"uptime"`
OS string `json:"os"`
Arch string `json:"arch"`
}
// Health returns server health status.
func (s *SystemService) Health(_ context.Context) *HealthStatus {
return &HealthStatus{
Status: "healthy",
Version: Version,
GoVersion: runtime.Version(),
Uptime: time.Since(s.startTime).Round(time.Second).String(),
OS: runtime.GOOS,
Arch: runtime.GOARCH,
}
}
// VersionInfo holds version information.
type VersionInfo struct {
Version string `json:"version"`
GitCommit string `json:"git_commit"`
BuildDate string `json:"build_date"`
GoVersion string `json:"go_version"`
}
// GetVersion returns version information.
func (s *SystemService) GetVersion() *VersionInfo {
return &VersionInfo{
Version: Version,
GitCommit: GitCommit,
BuildDate: BuildDate,
GoVersion: runtime.Version(),
}
}
// DashboardData holds summary data for the system dashboard.
type DashboardData struct {
Health *HealthStatus `json:"health"`
FactStats *memory.FactStoreStats `json:"fact_stats,omitempty"`
}
// Dashboard returns a summary of all system metrics.
func (s *SystemService) Dashboard(ctx context.Context) (*DashboardData, error) {
data := &DashboardData{
Health: s.Health(ctx),
}
if s.factStore != nil {
stats, err := s.factStore.Stats(ctx)
if err != nil {
return nil, fmt.Errorf("get fact stats: %w", err)
}
data.FactStats = stats
}
return data, nil
}

View file

@ -0,0 +1,94 @@
package tools
import (
"context"
"testing"
"github.com/sentinel-community/gomcp/internal/infrastructure/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestSystemService(t *testing.T) *SystemService {
t.Helper()
db, err := sqlite.OpenMemory()
require.NoError(t, err)
t.Cleanup(func() { db.Close() })
repo, err := sqlite.NewFactRepo(db)
require.NoError(t, err)
return NewSystemService(repo)
}
func TestSystemService_Health(t *testing.T) {
svc := newTestSystemService(t)
ctx := context.Background()
health := svc.Health(ctx)
require.NotNil(t, health)
assert.Equal(t, "healthy", health.Status)
assert.NotEmpty(t, health.GoVersion)
assert.NotEmpty(t, health.Version)
assert.NotEmpty(t, health.OS)
assert.NotEmpty(t, health.Arch)
assert.NotEmpty(t, health.Uptime)
}
func TestSystemService_GetVersion(t *testing.T) {
svc := newTestSystemService(t)
ver := svc.GetVersion()
require.NotNil(t, ver)
assert.NotEmpty(t, ver.Version)
assert.NotEmpty(t, ver.GoVersion)
assert.Equal(t, Version, ver.Version)
assert.Equal(t, GitCommit, ver.GitCommit)
assert.Equal(t, BuildDate, ver.BuildDate)
}
func TestSystemService_Dashboard(t *testing.T) {
svc := newTestSystemService(t)
ctx := context.Background()
data, err := svc.Dashboard(ctx)
require.NoError(t, err)
require.NotNil(t, data)
assert.NotNil(t, data.Health)
assert.Equal(t, "healthy", data.Health.Status)
assert.NotNil(t, data.FactStats)
assert.Equal(t, 0, data.FactStats.TotalFacts)
}
func TestSystemService_Dashboard_WithFacts(t *testing.T) {
svc := newTestSystemService(t)
ctx := context.Background()
// Add facts through the underlying store.
factSvc := NewFactService(svc.factStore, nil)
_, _ = factSvc.AddFact(ctx, AddFactParams{Content: "f1", Level: 0, Domain: "core"})
_, _ = factSvc.AddFact(ctx, AddFactParams{Content: "f2", Level: 1, Domain: "backend"})
data, err := svc.Dashboard(ctx)
require.NoError(t, err)
assert.Equal(t, 2, data.FactStats.TotalFacts)
}
func TestSystemService_Dashboard_NilFactStore(t *testing.T) {
svc := &SystemService{factStore: nil}
data, err := svc.Dashboard(context.Background())
require.NoError(t, err)
assert.NotNil(t, data.Health)
assert.Nil(t, data.FactStats)
}
func TestSystemService_Uptime(t *testing.T) {
svc := newTestSystemService(t)
ctx := context.Background()
h1 := svc.Health(ctx)
assert.NotEmpty(t, h1.Uptime)
// Uptime should be a parseable duration string like "0s" or "1ms".
assert.Contains(t, h1.Uptime, "s")
}