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