Zion Boggan
repos/TreeTrace/src/report.js
zionboggan.com ↗
195 lines · javascript
History for this file →
1
import { analyzeTree, latestByTime, renderRejectionsJson } from './analyze.js';
2
import { plural, truncate, escapeMd } from './util.js';
3
import { REPO_URL } from './config.js';
4
 
5
export function renderReportMarkdown(tree, opts = {}) {
6
  const projectName = opts.projectName || 'project';
7
  const generatedAt = opts.generatedAt || new Date().toISOString();
8
  const analysis = analyzeTree(tree);
9
  const lines = [];
10
 
11
  lines.push(`# TreeTrace Report - ${escapeMd(projectName)}`);
12
  lines.push('');
13
  lines.push(`Generated: ${generatedAt}`);
14
  lines.push('');
15
 
16
  lines.push('## Session summary');
17
  lines.push('');
18
  const { promptCount, rawPromptCount } = tree.stats;
19
  const foldedTurns = (rawPromptCount || promptCount) - promptCount;
20
  const tc = analysis.summary.tierCounts || { verified: 0, high: 0, confirmed: 0, inferred: 0 };
21
  const promptPart =
22
    foldedTurns > 0
23
      ? `Prompts: ${promptCount} (from ${rawPromptCount} raw turns)`
24
      : `Prompts: ${promptCount}`;
25
  const sessionParts = [
26
    promptPart,
27
    `Sessions: ${tree.stats.sessionCount}`,
28
    tree.stats.days ? `Span: ${plural(tree.stats.days, 'day')}` : null,
29
    tree.stats.toolUses ? `Tool calls: ${tree.stats.toolUses.toLocaleString()}` : null,
30
    tree.stats.filesTouched ? `Files touched: ${tree.stats.filesTouched}` : null,
31
  ].filter(Boolean);
32
  lines.push(`- ${sessionParts.join('  ')}`);
33
  lines.push(
34
    `- Failure signals: ${analysis.summary.totalFailureSignals} (verified ${tc.verified}, high ${tc.high || 0}, confirmed ${tc.confirmed}, inferred ${tc.inferred})`
35
  );
36
  if (tree.stats.corrections) lines.push(`- Corrections: ${tree.stats.corrections}`);
37
  if (tree.stats.abandonedBranches) lines.push(`- Abandoned branches: ${tree.stats.abandonedBranches}`);
38
  if (tree.stats.rejections) {
39
    const byKind = tree.stats.rejectionsByKind || {};
40
    const breakdown = Object.entries(byKind)
41
      .sort((a, b) => b[1] - a[1])
42
      .map(([k, v]) => `${k.replace(/_/g, ' ')}: ${v}`)
43
      .join(', ');
44
    lines.push(`- Rejections: ${tree.stats.rejections}${breakdown ? ` (${breakdown})` : ''}`);
45
  }
46
  {
47
    const allModels = [...new Set([
48
      ...(tree.stats.models || []),
49
      ...(analysis.summary.models || []),
50
    ])].filter(Boolean);
51
    if (allModels.length) lines.push(`- Models seen: ${allModels.join(', ')}`);
52
  }
53
  if (analysis.summary.thinkingBlocks) {
54
    lines.push(`- Reasoning blocks captured: ${analysis.summary.thinkingBlocks}`);
55
  }
56
  lines.push(`- Eval candidates: ${analysis.summary.evalCandidates}`);
57
  lines.push(`- Lessons: ${analysis.summary.lessons}`);
58
  lines.push('');
59
 
60
  lines.push('## Output map');
61
  lines.push('');
62
  lines.push('| File | Purpose |');
63
  lines.push('|------|---------|');
64
  lines.push('| `TREETRACE_REPORT.md` | this file |');
65
  lines.push('| `PROMPT_TREE.md` | prompt lineage + replay pack |');
66
  lines.push('| `.treetrace/tree.json` | canonical schema |');
67
  lines.push('| `.treetrace/failures.json` | labels + correction chains |');
68
  lines.push('| `.treetrace/rejections.json` | typed rejections/refusals/declines (v0.3) |');
69
  lines.push('| `.treetrace/hallucinations.json` | unresolved references |');
70
  lines.push('| `.treetrace/lessons.md` | correction memory |');
71
  lines.push('| `.treetrace/evals.jsonl` | regression eval cases |');
72
  lines.push('| `.treetrace/agent-memory.md` | next-agent memory pack |');
73
  lines.push('');
74
 
75
  if (analysis.failures.length) {
76
    lines.push('## Failure signals');
77
    lines.push('');
78
    for (const { type, count } of analysis.summary.topFailureTypes) {
79
      lines.push(`- ${type}: ${count}`);
80
    }
81
    lines.push('');
82
    for (const failure of analysis.failures.slice(0, 8)) {
83
      const meta = [failure.tier, confidencePct(failure.confidence), failure.model].filter(Boolean).join(', ');
84
      const nodeId = failure.firstSeenNodeId ? ` [${failure.firstSeenNodeId}]` : '';
85
      const evidence = failure.evidence ? ` Evidence: ${escapeMd(truncate(failure.evidence, 180))}` : '';
86
      lines.push(`- ${failure.id}${nodeId} (${failure.type}, ${meta}): ${escapeMd(failure.summary)}${evidence}`);
87
    }
88
    if (analysis.failures.length > 8) {
89
      lines.push(`- ... ${analysis.failures.length - 8} more in .treetrace/failures.json`);
90
    }
91
    lines.push('');
92
  }
93
 
94
  const securityTrail = analysis.failures.filter((f) => f.type === 'security_or_privacy_risk');
95
  if (securityTrail.length) {
96
    const rank = { verified: 4, high: 3, confirmed: 2, inferred: 1 };
97
    securityTrail.sort((a, b) => (rank[b.tier] || 0) - (rank[a.tier] || 0));
98
    lines.push('## Security audit trail');
99
    lines.push('');
100
    for (const f of securityTrail.slice(0, 12)) {
101
      const tag = f.tier === 'inferred' ? 'stated intent' : f.tier;
102
      const nodeId = f.firstSeenNodeId ? ` [${f.firstSeenNodeId}]` : '';
103
      lines.push(`- (${tag})${nodeId} ${escapeMd(f.evidence)}${f.model ? ` (${f.model})` : ''}`);
104
    }
105
    lines.push('');
106
  }
107
 
108
  if (analysis.correctionChains && analysis.correctionChains.length) {
109
    lines.push('## Correction chains');
110
    lines.push('');
111
    lines.push('Failure turns that received a human correction, with resolution status.');
112
    lines.push('');
113
    for (const chain of analysis.correctionChains.slice(0, 10)) {
114
      const resolved = chain.resolvedNodeId ? ` -> resolved [${chain.resolvedNodeId}]` : ' -> unresolved';
115
      lines.push(`- ${chain.id} (${chain.failureType}, ${chain.confidence}): failure [${chain.failureNodeId}] -> correction [${chain.correctionNodeId}]${resolved}`);
116
    }
117
    if (analysis.correctionChains.length > 10) {
118
      lines.push(`- ... ${analysis.correctionChains.length - 10} more in .treetrace/failures.json`);
119
    }
120
    lines.push('');
121
  }
122
 
123
  const rejectionsView = renderRejectionsJson(tree, opts);
124
  if (rejectionsView.summary.total) {
125
    lines.push('## Rejections');
126
    lines.push('');
127
    lines.push('Typed rejection / refusal / decline events captured on the session. Each one is also surfaced as a failure signal of the mapped type.');
128
    lines.push('');
129
    const byKind = rejectionsView.summary.byKind || {};
130
    const breakdown = Object.entries(byKind)
131
      .sort((a, b) => b[1] - a[1])
132
      .map(([k, v]) => `${k.replace(/_/g, ' ')} (${v})`)
133
      .join(', ');
134
    lines.push(`- Total: ${rejectionsView.summary.total}${breakdown ? ` - ${breakdown}` : ''}`);
135
    lines.push('');
136
    for (const r of rejectionsView.rejections.slice(0, 12)) {
137
      const nodeId = r.nodeId ? ` [${r.nodeId}]` : '';
138
      const pct = `${Math.round((r.confidence || 0) * 100)}%`;
139
      const ev = r.evidence ? ` - ${escapeMd(truncate(r.evidence, 160))}` : '';
140
      lines.push(`- (${r.kind}, ${pct})${nodeId}${ev}`);
141
    }
142
    if (rejectionsView.rejections.length > 12) {
143
      lines.push(`- ... ${rejectionsView.rejections.length - 12} more in .treetrace/rejections.json`);
144
    }
145
    lines.push('');
146
  }
147
 
148
  lines.push('## Artifacts');
149
  lines.push('');
150
  lines.push('See: `PROMPT_TREE.md` · `.treetrace/lessons.md` · `.treetrace/agent-memory.md` · handoff: run `treetrace --handoff`');
151
 
152
  lines.push('---');
153
  lines.push(`Generated by [treetrace](${REPO_URL})${opts.version ? ` v${opts.version}` : ''}.`);
154
  lines.push('');
155
 
156
  return lines.join('\n');
157
}
158
 
159
export function renderTerminalSummary(tree, opts = {}) {
160
  const projectName = opts.projectName || 'project';
161
  const analysis = analyzeTree(tree);
162
  const accepted = tree.nodes.filter((n) => n.status !== 'abandoned');
163
  const lastAccepted = latestByTime(accepted);
164
  const lines = [];
165
 
166
  lines.push(`TreeTrace summary - ${projectName}`);
167
  lines.push('');
168
  lines.push(
169
    `${plural(tree.stats.promptCount, 'prompt')} across ${plural(tree.stats.sessionCount, 'session')} ` +
170
      `| ${analysis.summary.totalFailureSignals} failure signals ` +
171
      `| ${analysis.summary.lessons} lessons ` +
172
      `| ${analysis.summary.evalCandidates} eval candidates`
173
  );
174
  if (lastAccepted) {
175
    lines.push('');
176
    lines.push(`Latest accepted direction: ${truncate(lastAccepted.text.replace(/\s+/g, ' '), 280)}`);
177
  }
178
  if (analysis.lessons.length) {
179
    lines.push('');
180
    lines.push('Top lessons:');
181
    for (const lesson of analysis.lessons.slice(0, 3)) {
182
      lines.push(`- ${truncate(lesson.text.replace(/\s+/g, ' '), 240)}`);
183
    }
184
  }
185
  lines.push('');
186
  lines.push('Human report: TREETRACE_REPORT.md');
187
  lines.push('Stream it in the terminal with: treetrace --report');
188
  lines.push('');
189
 
190
  return lines.join('\n');
191
}
192
 
193
function confidencePct(confidence) {
194
  return `${Math.round(confidence * 100)}%`;
195
}