~cytrogen/gstack

ref: db35b8e5bffb596144c4c7f4a3b7eb5c078edaaf gstack/bin/gstack-learnings-search -rwxr-xr-x 4.0 KiB
db35b8e5 — Garry Tan feat: session intelligence roadmap + design doc (#727) 8 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#!/usr/bin/env bash
# gstack-learnings-search — read and filter project learnings
# Usage: gstack-learnings-search [--type TYPE] [--query KEYWORD] [--limit N] [--cross-project]
#
# Reads ~/.gstack/projects/$SLUG/learnings.jsonl, applies confidence decay,
# resolves duplicates (latest winner per key+type), and outputs formatted text.
# Exit 0 silently if no learnings file exists.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"

TYPE=""
QUERY=""
LIMIT=10
CROSS_PROJECT=false

while [[ $# -gt 0 ]]; do
  case "$1" in
    --type) TYPE="$2"; shift 2 ;;
    --query) QUERY="$2"; shift 2 ;;
    --limit) LIMIT="$2"; shift 2 ;;
    --cross-project) CROSS_PROJECT=true; shift ;;
    *) shift ;;
  esac
done

LEARNINGS_FILE="$GSTACK_HOME/projects/$SLUG/learnings.jsonl"

# Collect all JSONL files to search
FILES=()
[ -f "$LEARNINGS_FILE" ] && FILES+=("$LEARNINGS_FILE")

if [ "$CROSS_PROJECT" = true ]; then
  # Add other projects' learnings (max 5, sorted by mtime)
  for f in $(find "$GSTACK_HOME/projects" -name "learnings.jsonl" -not -path "*/$SLUG/*" 2>/dev/null | head -5); do
    FILES+=("$f")
  done
fi

if [ ${#FILES[@]} -eq 0 ]; then
  exit 0
fi

# Process all files through bun for JSON parsing, decay, dedup, filtering
cat "${FILES[@]}" 2>/dev/null | bun -e "
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
const now = Date.now();
const type = '${TYPE}';
const query = '${QUERY}'.toLowerCase();
const limit = ${LIMIT};
const slug = '${SLUG}';

const entries = [];
for (const line of lines) {
  try {
    const e = JSON.parse(line);
    if (!e.key || !e.type) continue;

    // Apply confidence decay: observed/inferred lose 1pt per 30 days
    let conf = e.confidence || 5;
    if (e.source === 'observed' || e.source === 'inferred') {
      const days = Math.floor((now - new Date(e.ts).getTime()) / 86400000);
      conf = Math.max(0, conf - Math.floor(days / 30));
    }
    e._effectiveConfidence = conf;

    // Determine if this is from the current project or cross-project
    // Cross-project entries are tagged for display
    e._crossProject = !line.includes(slug) && '${CROSS_PROJECT}' === 'true';

    entries.push(e);
  } catch {}
}

// Dedup: latest winner per key+type
const seen = new Map();
for (const e of entries) {
  const dk = e.key + '|' + e.type;
  const existing = seen.get(dk);
  if (!existing || new Date(e.ts) > new Date(existing.ts)) {
    seen.set(dk, e);
  }
}
let results = Array.from(seen.values());

// Filter by type
if (type) results = results.filter(e => e.type === type);

// Filter by query
if (query) results = results.filter(e =>
  (e.key || '').toLowerCase().includes(query) ||
  (e.insight || '').toLowerCase().includes(query) ||
  (e.files || []).some(f => f.toLowerCase().includes(query))
);

// Sort by effective confidence desc, then recency
results.sort((a, b) => {
  if (b._effectiveConfidence !== a._effectiveConfidence) return b._effectiveConfidence - a._effectiveConfidence;
  return new Date(b.ts).getTime() - new Date(a.ts).getTime();
});

// Limit
results = results.slice(0, limit);

if (results.length === 0) process.exit(0);

// Format output
const byType = {};
for (const e of results) {
  const t = e.type || 'unknown';
  if (!byType[t]) byType[t] = [];
  byType[t].push(e);
}

// Summary line
const counts = Object.entries(byType).map(([t, arr]) => arr.length + ' ' + t + (arr.length > 1 ? 's' : ''));
console.log('LEARNINGS: ' + results.length + ' loaded (' + counts.join(', ') + ')');
console.log('');

for (const [t, arr] of Object.entries(byType)) {
  console.log('## ' + t.charAt(0).toUpperCase() + t.slice(1) + 's');
  for (const e of arr) {
    const cross = e._crossProject ? ' [cross-project]' : '';
    const files = e.files?.length ? ' (files: ' + e.files.join(', ') + ')' : '';
    console.log('- [' + e.key + '] (confidence: ' + e._effectiveConfidence + '/10, ' + e.source + ', ' + (e.ts || '').split('T')[0] + ')' + cross);
    console.log('  ' + e.insight + files);
  }
  console.log('');
}
" 2>/dev/null || exit 0