Zion Boggan
repos/TreeTrace/src/handoff.js
zionboggan.com ↗
85 lines · javascript
History for this file →
1
import { truncate, escapeMdTags } from './util.js';
2
import { analyzeTree, isStrategicDirection, latestByTime } from './analyze.js';
3
 
4
export function renderHandoff(tree, opts = {}) {
5
  const { projectName } = opts;
6
  const { nodes, stats } = tree;
7
  const analysis = analyzeTree(tree);
8
  const lines = [];
9
 
10
  const root = nodes.find((n) => n.kind === 'root') || nodes[0];
11
  const accepted = nodes.filter((n) => n.status !== 'abandoned');
12
  const lastCheckpoint = latestByTime(accepted.filter((n) => n.kind === 'checkpoint'));
13
  const lastAccepted = latestByTime(accepted);
14
 
15
  lines.push(`# Handoff brief: ${escapeMdTags(projectName)}`);
16
  lines.push(`${stats.promptCount} ${plural(stats.promptCount, 'prompt')} · ${stats.sessionCount} ${plural(stats.sessionCount, 'session')}`);
17
  lines.push('');
18
 
19
  if (root) {
20
    lines.push('## Original goal');
21
    lines.push('');
22
    lines.push(escapeMdTags(root.text.trim()));
23
    lines.push('');
24
  }
25
 
26
  lines.push('## Where things stand');
27
  lines.push('');
28
  if (lastCheckpoint) {
29
    lines.push(`Last checkpoint: ${escapeMdTags(lastCheckpoint.text.trim())}`);
30
    if (lastAccepted && lastAccepted !== lastCheckpoint) lines.push('');
31
  }
32
  if (lastAccepted && lastAccepted !== lastCheckpoint) {
33
    lines.push(`Most recent accepted direction: ${escapeMdTags(lastAccepted.text.trim())}`);
34
  }
35
  lines.push('');
36
 
37
  const decisions = accepted.filter(
38
    (n) => (n.kind === 'direction' || n.kind === 'scope-change') && isStrategicDirection(n)
39
  );
40
  if (decisions.length) {
41
    lines.push('## Accepted decisions');
42
    lines.push('');
43
    decisions.forEach((n, i) => lines.push(`${i + 1}. ${escapeMdTags(truncate(n.text.replace(/\s+/g, ' '), 360))}`));
44
    lines.push('');
45
  }
46
 
47
  const corrections = accepted.filter((n) => n.kind === 'correction');
48
  if (corrections.length) {
49
    lines.push('## Constraints');
50
    lines.push('');
51
    corrections.forEach((n) => lines.push(`- ${escapeMdTags(truncate(n.text.replace(/\s+/g, ' '), 300))}`));
52
    lines.push('');
53
  }
54
 
55
  const abandoned = nodes.filter(
56
    (n) => n.status === 'abandoned' && (!n.parent || n.parent.status !== 'abandoned')
57
  );
58
  if (abandoned.length) {
59
    lines.push('## Dead ends');
60
    lines.push('');
61
    abandoned.forEach((n) => lines.push(`- ${escapeMdTags(truncate(n.text.replace(/\s+/g, ' '), 300))}`));
62
    lines.push('');
63
  }
64
 
65
  if (analysis.lessons.length) {
66
    lines.push('## Lessons');
67
    lines.push('');
68
    analysis.lessons.slice(0, 6).forEach((lesson) => {
69
      lines.push(`- ${escapeMdTags(lesson.title)}: ${escapeMdTags(truncate(compactLessonText(lesson.text), 320))}`);
70
    });
71
    lines.push('');
72
  }
73
 
74
  return lines.join('\n');
75
}
76
 
77
function plural(count, singular) {
78
  return count === 1 ? singular : `${singular}s`;
79
}
80
 
81
function compactLessonText(text) {
82
  const normalized = String(text || '').replace(/\s+/g, ' ').trim();
83
  const evidenceAt = normalized.indexOf('Specifically:');
84
  return evidenceAt === -1 ? normalized : normalized.slice(evidenceAt + 'Specifically:'.length).trim();
85
}