gomcp/internal/domain/entropy/gate.go

232 lines
6.5 KiB
Go

// Package entropy implements the Entropy Gate — a DIP H0.3 component
// that measures Shannon entropy of text signals and blocks anomalous patterns.
//
// Core thesis: destructive intent exhibits higher entropy (noise/chaos),
// while constructive intent exhibits lower entropy (structured/coherent).
// The gate measures entropy at each processing step and triggers apoptosis
// (pipeline kill) when entropy exceeds safe thresholds.
package entropy
import (
"fmt"
"math"
"strings"
"unicode/utf8"
)
// GateConfig configures the entropy gate thresholds.
type GateConfig struct {
// MaxEntropy is the maximum allowed Shannon entropy (bits/char).
// Typical English text: 3.5-4.5 bits/char.
// Random/adversarial text: 5.5+ bits/char.
// Default: 5.0 (generous for multilingual content).
MaxEntropy float64
// MaxEntropyGrowth is the maximum allowed entropy increase
// between iterations. If entropy grows faster than this,
// the signal is likely diverging (destructive recursion).
// Default: 0.5 bits/char per iteration.
MaxEntropyGrowth float64
// MinTextLength is the minimum text length to analyze.
// Shorter texts have unreliable entropy. Default: 20.
MinTextLength int
// MaxIterationsWithoutDecline triggers apoptosis if entropy
// hasn't decreased in N consecutive iterations. Default: 3.
MaxIterationsWithoutDecline int
}
// DefaultGateConfig returns sensible defaults.
func DefaultGateConfig() GateConfig {
return GateConfig{
MaxEntropy: 5.0,
MaxEntropyGrowth: 0.5,
MinTextLength: 20,
MaxIterationsWithoutDecline: 3,
}
}
// GateResult holds the result of an entropy gate check.
type GateResult struct {
Entropy float64 `json:"entropy"` // Shannon entropy in bits/char
CharCount int `json:"char_count"` // Number of characters analyzed
UniqueChars int `json:"unique_chars"` // Number of unique characters
IsAllowed bool `json:"is_allowed"` // Signal passed the gate
IsBlocked bool `json:"is_blocked"` // Signal was blocked (apoptosis)
BlockReason string `json:"block_reason,omitempty"` // Why it was blocked
EntropyDelta float64 `json:"entropy_delta"` // Change from previous measurement
}
// Gate performs entropy-based signal analysis.
type Gate struct {
cfg GateConfig
history []float64 // entropy history across iterations
iterationsFlat int // consecutive iterations without entropy decline
}
// NewGate creates a new Entropy Gate.
func NewGate(cfg *GateConfig) *Gate {
c := DefaultGateConfig()
if cfg != nil {
if cfg.MaxEntropy > 0 {
c.MaxEntropy = cfg.MaxEntropy
}
if cfg.MaxEntropyGrowth > 0 {
c.MaxEntropyGrowth = cfg.MaxEntropyGrowth
}
if cfg.MinTextLength > 0 {
c.MinTextLength = cfg.MinTextLength
}
if cfg.MaxIterationsWithoutDecline > 0 {
c.MaxIterationsWithoutDecline = cfg.MaxIterationsWithoutDecline
}
}
return &Gate{cfg: c}
}
// Check evaluates a text signal and returns whether it should pass.
// Call this on each iteration of a recursive loop.
func (g *Gate) Check(text string) *GateResult {
charCount := utf8.RuneCountInString(text)
result := &GateResult{
CharCount: charCount,
IsAllowed: true,
}
// Too short for reliable analysis — allow by default.
if charCount < g.cfg.MinTextLength {
result.Entropy = 0
return result
}
// Compute Shannon entropy.
e := ShannonEntropy(text)
result.Entropy = e
result.UniqueChars = countUniqueRunes(text)
// Check 1: Absolute entropy threshold.
if e > g.cfg.MaxEntropy {
result.IsAllowed = false
result.IsBlocked = true
result.BlockReason = fmt.Sprintf(
"entropy %.3f exceeds max %.3f (signal too chaotic)",
e, g.cfg.MaxEntropy)
}
// Check 2: Entropy growth rate.
if len(g.history) > 0 {
prev := g.history[len(g.history)-1]
result.EntropyDelta = e - prev
if result.EntropyDelta > g.cfg.MaxEntropyGrowth {
result.IsAllowed = false
result.IsBlocked = true
result.BlockReason = fmt.Sprintf(
"entropy growth %.3f exceeds max %.3f (divergent recursion)",
result.EntropyDelta, g.cfg.MaxEntropyGrowth)
}
// Track iterations without decline.
// Use epsilon tolerance because ShannonEntropy iterates a map,
// and Go randomizes map iteration order, causing micro-different
// float64 results for the same text due to addition ordering.
const epsilon = 1e-9
if e >= prev-epsilon {
g.iterationsFlat++
} else {
g.iterationsFlat = 0
}
// Check 3: Stagnation (recursive collapse).
if g.iterationsFlat >= g.cfg.MaxIterationsWithoutDecline {
result.IsAllowed = false
result.IsBlocked = true
result.BlockReason = fmt.Sprintf(
"entropy stagnant for %d iterations (recursive collapse)",
g.iterationsFlat)
}
}
g.history = append(g.history, e)
return result
}
// Reset clears the gate state for a new pipeline.
func (g *Gate) Reset() {
g.history = nil
g.iterationsFlat = 0
}
// History returns the entropy measurements across iterations.
func (g *Gate) History() []float64 {
h := make([]float64, len(g.history))
copy(h, g.history)
return h
}
// ShannonEntropy computes Shannon entropy in bits per character.
// H(X) = -Σ p(x) * log2(p(x))
func ShannonEntropy(text string) float64 {
if len(text) == 0 {
return 0
}
// Count character frequencies.
freq := make(map[rune]int)
total := 0
for _, r := range text {
freq[r]++
total++
}
// Compute entropy.
var h float64
for _, count := range freq {
p := float64(count) / float64(total)
if p > 0 {
h -= p * math.Log2(p)
}
}
return h
}
// AnalyzeText provides a comprehensive entropy analysis of text.
func AnalyzeText(text string) map[string]interface{} {
charCount := utf8.RuneCountInString(text)
wordCount := len(strings.Fields(text))
uniqueChars := countUniqueRunes(text)
entropy := ShannonEntropy(text)
// Theoretical maximum entropy for this character set.
maxEntropy := 0.0
if uniqueChars > 0 {
maxEntropy = math.Log2(float64(uniqueChars))
}
// Redundancy: how much structure the text has.
// 0 = maximum entropy (random), 1 = minimum entropy (all same char).
redundancy := 0.0
if maxEntropy > 0 {
redundancy = 1.0 - (entropy / maxEntropy)
}
return map[string]interface{}{
"entropy": entropy,
"max_entropy": maxEntropy,
"redundancy": redundancy,
"char_count": charCount,
"word_count": wordCount,
"unique_chars": uniqueChars,
"bits_per_word": 0.0,
}
}
func countUniqueRunes(s string) int {
seen := make(map[rune]struct{})
for _, r := range s {
seen[r] = struct{}{}
}
return len(seen)
}