mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-24 20:06:21 +02:00
181 lines
4.6 KiB
Go
181 lines
4.6 KiB
Go
// Copyright 2026 Syntrex Lab. All rights reserved.
|
|
// Use of this source code is governed by an Apache-2.0 license
|
|
// that can be found in the LICENSE file.
|
|
|
|
// Package causal defines domain entities for causal reasoning chains.
|
|
package causal
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// NodeType classifies causal chain nodes.
|
|
type NodeType string
|
|
|
|
const (
|
|
NodeDecision NodeType = "decision"
|
|
NodeReason NodeType = "reason"
|
|
NodeConsequence NodeType = "consequence"
|
|
NodeConstraint NodeType = "constraint"
|
|
NodeAlternative NodeType = "alternative"
|
|
NodeAssumption NodeType = "assumption" // DB-compatible
|
|
)
|
|
|
|
// IsValid checks if the node type is known.
|
|
func (nt NodeType) IsValid() bool {
|
|
switch nt {
|
|
case NodeDecision, NodeReason, NodeConsequence, NodeConstraint, NodeAlternative, NodeAssumption:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// EdgeType classifies causal chain edges.
|
|
type EdgeType string
|
|
|
|
const (
|
|
EdgeJustifies EdgeType = "justifies"
|
|
EdgeCauses EdgeType = "causes"
|
|
EdgeConstrains EdgeType = "constrains"
|
|
)
|
|
|
|
// IsValid checks if the edge type is known.
|
|
func (et EdgeType) IsValid() bool {
|
|
switch et {
|
|
case EdgeJustifies, EdgeCauses, EdgeConstrains:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Node represents a single node in a causal chain.
|
|
type Node struct {
|
|
ID string `json:"id"`
|
|
Type NodeType `json:"type"`
|
|
Content string `json:"content"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// NewNode creates a new Node with a generated ID and timestamp.
|
|
func NewNode(nodeType NodeType, content string) *Node {
|
|
return &Node{
|
|
ID: generateID(),
|
|
Type: nodeType,
|
|
Content: content,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
}
|
|
|
|
// Validate checks required fields.
|
|
func (n *Node) Validate() error {
|
|
if n.ID == "" {
|
|
return errors.New("node ID is required")
|
|
}
|
|
if n.Content == "" {
|
|
return errors.New("node content is required")
|
|
}
|
|
if !n.Type.IsValid() {
|
|
return fmt.Errorf("invalid node type: %s", n.Type)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Edge represents a directed relationship between two nodes.
|
|
type Edge struct {
|
|
ID string `json:"id"`
|
|
FromID string `json:"from_id"`
|
|
ToID string `json:"to_id"`
|
|
Type EdgeType `json:"type"`
|
|
}
|
|
|
|
// NewEdge creates a new Edge with a generated ID.
|
|
func NewEdge(fromID, toID string, edgeType EdgeType) *Edge {
|
|
return &Edge{
|
|
ID: generateID(),
|
|
FromID: fromID,
|
|
ToID: toID,
|
|
Type: edgeType,
|
|
}
|
|
}
|
|
|
|
// Validate checks required fields and constraints.
|
|
func (e *Edge) Validate() error {
|
|
if e.ID == "" {
|
|
return errors.New("edge ID is required")
|
|
}
|
|
if e.FromID == "" {
|
|
return errors.New("edge from_id is required")
|
|
}
|
|
if e.ToID == "" {
|
|
return errors.New("edge to_id is required")
|
|
}
|
|
if !e.Type.IsValid() {
|
|
return fmt.Errorf("invalid edge type: %s", e.Type)
|
|
}
|
|
if e.FromID == e.ToID {
|
|
return errors.New("self-loop edges are not allowed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Chain represents a complete causal chain for a decision.
|
|
type Chain struct {
|
|
Decision *Node `json:"decision,omitempty"`
|
|
Reasons []*Node `json:"reasons,omitempty"`
|
|
Consequences []*Node `json:"consequences,omitempty"`
|
|
Constraints []*Node `json:"constraints,omitempty"`
|
|
Alternatives []*Node `json:"alternatives,omitempty"`
|
|
TotalNodes int `json:"total_nodes"`
|
|
}
|
|
|
|
// ToMermaid renders the chain as a Mermaid diagram.
|
|
func (c *Chain) ToMermaid() string {
|
|
var sb strings.Builder
|
|
sb.WriteString("graph TD\n")
|
|
|
|
if c.Decision != nil {
|
|
fmt.Fprintf(&sb, " %s[\"%s\"]\n", c.Decision.ID, c.Decision.Content)
|
|
|
|
for _, r := range c.Reasons {
|
|
fmt.Fprintf(&sb, " %s[\"%s\"] -->|justifies| %s\n", r.ID, r.Content, c.Decision.ID)
|
|
}
|
|
for _, co := range c.Consequences {
|
|
fmt.Fprintf(&sb, " %s -->|causes| %s[\"%s\"]\n", c.Decision.ID, co.ID, co.Content)
|
|
}
|
|
for _, cn := range c.Constraints {
|
|
fmt.Fprintf(&sb, " %s[\"%s\"] -->|constrains| %s\n", cn.ID, cn.Content, c.Decision.ID)
|
|
}
|
|
for _, a := range c.Alternatives {
|
|
fmt.Fprintf(&sb, " %s[\"%s\"] -.->|alternative| %s\n", a.ID, a.Content, c.Decision.ID)
|
|
}
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// CausalStats holds aggregate statistics about the causal store.
|
|
type CausalStats struct {
|
|
TotalNodes int `json:"total_nodes"`
|
|
TotalEdges int `json:"total_edges"`
|
|
ByType map[NodeType]int `json:"by_type"`
|
|
}
|
|
|
|
// CausalStore defines the interface for causal chain persistence.
|
|
type CausalStore interface {
|
|
AddNode(ctx context.Context, node *Node) error
|
|
AddEdge(ctx context.Context, edge *Edge) error
|
|
GetChain(ctx context.Context, query string, maxDepth int) (*Chain, error)
|
|
Stats(ctx context.Context) (*CausalStats, error)
|
|
}
|
|
|
|
func generateID() string {
|
|
b := make([]byte, 16)
|
|
_, _ = rand.Read(b)
|
|
return hex.EncodeToString(b)
|
|
}
|