directive.go raw

   1  package oracle
   2  
   3  import (
   4  	"fmt"
   5  	"strings"
   6  )
   7  
   8  // GenerateDirectives builds strategy directives from a reading's hexagram
   9  // state, source identifier, and engagement style.
  10  //
  11  // The primary directive comes from the inner+outer trigram decomposition.
  12  // Additional directives come from each changing line, identifying which
  13  // specific dimension is in flux.
  14  func GenerateDirectives(r *Reading, source string, style EngagementStyle) []Directive {
  15  	var directives []Directive
  16  
  17  	inner := r.InnerTrigram()
  18  	outer := r.OuterTrigram()
  19  	domain := TrigramToDomain(outer)
  20  	intention := TrigramToIntention(inner)
  21  
  22  	// Primary directive: intention + domain + source + style.
  23  	primary := Directive{
  24  		Type:        directiveTypeForSource(source),
  25  		Query:       buildQuery(intention, domain, source, style),
  26  		Domain:      domain,
  27  		Intention:   intention,
  28  		Style:       style,
  29  		ChangingBit: -1, // primary, not from a changing line
  30  	}
  31  	directives = append(directives, primary)
  32  
  33  	// Changing line directives: each changing line adds a dimension-specific
  34  	// follow-up identifying what is in flux.
  35  	for _, lineIdx := range r.ChangingLines() {
  36  		bitWithinTrigram := lineIdx % 3
  37  		isInner := lineIdx < 3
  38  
  39  		d := Directive{
  40  			Type:        directiveTypeForSource(source),
  41  			Query:       buildChangingLineQuery(bitWithinTrigram, isInner, source, style),
  42  			Domain:      domain,
  43  			Intention:   intention,
  44  			Style:       style,
  45  			ChangingBit: lineIdx,
  46  		}
  47  		directives = append(directives, d)
  48  	}
  49  
  50  	return directives
  51  }
  52  
  53  // directiveTypeForSource maps a data source identifier to the appropriate
  54  // directive type.
  55  func directiveTypeForSource(source string) DirectiveType {
  56  	switch source {
  57  	case "self", "walk", "local":
  58  		return DirectiveWalkWeight
  59  	case "forage", "web", "search":
  60  		return DirectiveSearch
  61  	case "nostr", "relay":
  62  		return DirectiveSubscribe
  63  	default:
  64  		return DirectiveFocus
  65  	}
  66  }
  67  
  68  // buildQuery constructs a strategy string from intention, domain, source, and style.
  69  // For walk/self sources this produces a concise focus descriptor; for search
  70  // sources it produces a natural language query.
  71  func buildQuery(intention IntentionType, domain DomainType, source string, style EngagementStyle) string {
  72  	keywords := DomainKeywords(domain)
  73  	dtype := directiveTypeForSource(source)
  74  
  75  	// Walk/self: concise focus tags, not a search sentence.
  76  	if dtype == DirectiveWalkWeight {
  77  		parts := []string{domainLabel(domain), intentionLabel(intention)}
  78  		if len(keywords) > 1 {
  79  			parts = append(parts, keywords[1]) // second keyword for variety
  80  		}
  81  		parts = append(parts, styleLabel(style))
  82  		return strings.Join(parts, "→")
  83  	}
  84  
  85  	// Search/subscribe: natural language query.
  86  	prefix := IntentionPrefix(intention)
  87  	var b strings.Builder
  88  	b.WriteString(prefix)
  89  	b.WriteString(" ")
  90  	b.WriteString(source)
  91  	if len(keywords) > 0 {
  92  		b.WriteString(" ")
  93  		b.WriteString(keywords[0])
  94  	}
  95  	switch style {
  96  	case StyleBroad:
  97  		b.WriteString(" overview")
  98  	case StyleTargeted:
  99  		b.WriteString(" detail")
 100  	case StyleReference:
 101  		b.WriteString(" reference")
 102  	case StyleSynthesis:
 103  		b.WriteString(" synthesis")
 104  	}
 105  	return b.String()
 106  }
 107  
 108  func domainLabel(d DomainType) string {
 109  	switch d {
 110  	case DomainEmpirical:
 111  		return "empirical"
 112  	case DomainEmerging:
 113  		return "emerging"
 114  	case DomainCritiques:
 115  		return "critiques"
 116  	case DomainApplications:
 117  		return "applied"
 118  	case DomainVisualization:
 119  		return "visual"
 120  	case DomainFoundations:
 121  		return "foundations"
 122  	case DomainMethodology:
 123  		return "method"
 124  	case DomainCanon:
 125  		return "canon"
 126  	default:
 127  		return "general"
 128  	}
 129  }
 130  
 131  func intentionLabel(i IntentionType) string {
 132  	switch i {
 133  	case IntentEmpirical:
 134  		return "evidence"
 135  	case IntentEmerging:
 136  		return "novelty"
 137  	case IntentCritiques:
 138  		return "failures"
 139  	case IntentApplications:
 140  		return "practice"
 141  	case IntentVisualization:
 142  		return "intuition"
 143  	case IntentFoundations:
 144  		return "axioms"
 145  	case IntentMethodology:
 146  		return "technique"
 147  	case IntentCanon:
 148  		return "standard"
 149  	default:
 150  		return "focus"
 151  	}
 152  }
 153  
 154  func styleLabel(s EngagementStyle) string {
 155  	switch s {
 156  	case StyleBroad:
 157  		return "broad"
 158  	case StyleTargeted:
 159  		return "targeted"
 160  	case StyleReference:
 161  		return "reference"
 162  	case StyleSynthesis:
 163  		return "synthesis"
 164  	default:
 165  		return "explore"
 166  	}
 167  }
 168  
 169  // buildChangingLineQuery constructs a follow-up query for a changing line.
 170  func buildChangingLineQuery(bit int, isInner bool, source string, style EngagementStyle) string {
 171  	dtype := directiveTypeForSource(source)
 172  
 173  	// Walk/self: short dimension tag.
 174  	if dtype == DirectiveWalkWeight {
 175  		dim := changingBitLabel(bit, isInner)
 176  		return dim + "→" + styleLabel(style)
 177  	}
 178  
 179  	// Search: natural language.
 180  	focus := ChangingBitFocus(bit, isInner)
 181  	return fmt.Sprintf("%s %s", focus, source)
 182  }
 183  
 184  func changingBitLabel(bit int, isInner bool) string {
 185  	scope := "source"
 186  	if isInner {
 187  		scope = "understanding"
 188  	}
 189  	switch bit {
 190  	case 0:
 191  		return "bonding:" + scope
 192  	case 1:
 193  		return "constraint:" + scope
 194  	case 2:
 195  		return "energy:" + scope
 196  	default:
 197  		return "flux:" + scope
 198  	}
 199  }
 200