enzyme.go raw

   1  package strategy
   2  
   3  import (
   4  	"io"
   5  	"strings"
   6  
   7  	"git.mleku.dev/mleku/dendrite/pkg/axiom"
   8  )
   9  
  10  // Enzyme decomposes The Art of War into multi-level strategy elements.
  11  type Enzyme struct{}
  12  
  13  // CanDigest returns true for text containing Art of War markers.
  14  func (Enzyme) CanDigest(sample []byte) bool {
  15  	s := string(sample)
  16  	return strings.Contains(s, "Sun Tz") || strings.Contains(s, "Art of War")
  17  }
  18  
  19  // Digest reads The Art of War and emits strategy elements at four levels.
  20  func (Enzyme) Digest(r io.Reader) <-chan axiom.Element {
  21  	ch := make(chan axiom.Element, 256)
  22  	go func() {
  23  		defer close(ch)
  24  
  25  		data, err := io.ReadAll(r)
  26  		if err != nil {
  27  			return
  28  		}
  29  
  30  		chapters := parseTextIntoChapters(string(data))
  31  		for _, chap := range chapters {
  32  			// Chapter-level element.
  33  			ch <- StrategyElement{
  34  				Content: chap.Title + ": " + summarize(chap.Text, 200),
  35  				Ch:      chap.Number,
  36  				Cat:     chap.Category,
  37  				Lvl:     LevelChapter,
  38  			}
  39  
  40  			// Principle-level: paragraphs.
  41  			for _, para := range splitParagraphs(chap.Text) {
  42  				if len(para) < 20 {
  43  					continue
  44  				}
  45  				ch <- StrategyElement{
  46  					Content: para,
  47  					Ch:      chap.Number,
  48  					Cat:     chap.Category,
  49  					Lvl:     LevelPrinciple,
  50  				}
  51  			}
  52  
  53  			// Maxim-level: extracted key sentences.
  54  			for _, maxim := range chap.Maxims {
  55  				ch <- StrategyElement{
  56  					Content: maxim.Text,
  57  					Ch:      chap.Number,
  58  					Cat:     chap.Category,
  59  					Lvl:     LevelMaxim,
  60  				}
  61  			}
  62  
  63  			// Tactic-level: short imperative phrases.
  64  			for _, tactic := range extractTactics(chap.Text) {
  65  				ch <- StrategyElement{
  66  					Content: tactic,
  67  					Ch:      chap.Number,
  68  					Cat:     chap.Category,
  69  					Lvl:     LevelTactic,
  70  				}
  71  			}
  72  		}
  73  	}()
  74  	return ch
  75  }
  76  
  77  // DigestEmbedded digests the built-in Art of War text.
  78  func DigestEmbedded() <-chan axiom.Element {
  79  	return (Enzyme{}).Digest(strings.NewReader(artOfWarText))
  80  }
  81  
  82  // parseTextIntoChapters splits arbitrary Art of War text into chapters.
  83  // Uses the same markers as the embedded text.
  84  func parseTextIntoChapters(text string) []Chapter {
  85  	lines := strings.Split(text, "\n")
  86  
  87  	type boundary struct {
  88  		line int
  89  		idx  int
  90  	}
  91  	var bounds []boundary
  92  	for i, line := range lines {
  93  		for j, def := range chapterDefs {
  94  			if strings.HasPrefix(line, def.marker) {
  95  				bounds = append(bounds, boundary{i, j})
  96  			}
  97  		}
  98  	}
  99  
 100  	result := make([]Chapter, 0, len(bounds))
 101  	for bi, b := range bounds {
 102  		end := len(lines)
 103  		if bi+1 < len(bounds) {
 104  			end = bounds[bi+1].line
 105  		}
 106  		var textLines []string
 107  		for _, l := range lines[b.line+1 : end] {
 108  			textLines = append(textLines, l)
 109  		}
 110  		text := strings.TrimSpace(strings.Join(textLines, "\n"))
 111  		def := chapterDefs[b.idx]
 112  		result = append(result, Chapter{
 113  			Number:   b.idx + 1,
 114  			Title:    def.title,
 115  			Category: def.category,
 116  			Text:     text,
 117  			Maxims:   extractMaxims(text, b.idx+1),
 118  		})
 119  	}
 120  	return result
 121  }
 122  
 123  // splitParagraphs splits text on blank lines.
 124  func splitParagraphs(text string) []string {
 125  	raw := strings.Split(text, "\n\n")
 126  	var result []string
 127  	for _, p := range raw {
 128  		p = strings.TrimSpace(p)
 129  		if p != "" {
 130  			result = append(result, p)
 131  		}
 132  	}
 133  	return result
 134  }
 135  
 136  // summarize returns the first n bytes of text, ending at a word boundary.
 137  func summarize(text string, n int) string {
 138  	if len(text) <= n {
 139  		return text
 140  	}
 141  	cut := strings.LastIndex(text[:n], " ")
 142  	if cut < 0 {
 143  		cut = n
 144  	}
 145  	return text[:cut] + "..."
 146  }
 147  
 148  // extractTactics identifies short imperative phrases from chapter text.
 149  func extractTactics(text string) []string {
 150  	sentences := splitSentences(text)
 151  	var tactics []string
 152  	for _, s := range sentences {
 153  		s = strings.TrimSpace(s)
 154  		words := strings.Fields(s)
 155  		// Tactics are short, punchy directives.
 156  		if len(words) >= 3 && len(words) <= 15 && isTactical(s) {
 157  			tactics = append(tactics, s)
 158  		}
 159  	}
 160  	return tactics
 161  }
 162  
 163  // isTactical checks if a short sentence is a tactical directive.
 164  func isTactical(s string) bool {
 165  	lower := strings.ToLower(s)
 166  	tactical := []string{
 167  		"attack", "defend", "retreat", "advance", "hold",
 168  		"strike", "feign", "entice", "crush", "separate",
 169  		"surround", "divide", "wait", "avoid", "pursue",
 170  		"do not", "never", "always", "beware",
 171  		"appear", "conceal", "hide", "reveal",
 172  	}
 173  	for _, kw := range tactical {
 174  		if strings.Contains(lower, kw) {
 175  			return true
 176  		}
 177  	}
 178  	return false
 179  }
 180