| 1 | import { REPO_URL, SCHEMA_VERSION } from './config.js'; |
| 2 | import { analyzeTree } from './analyze.js'; |
| 3 | |
| 4 | const RELATIONSHIP_BY_KIND = { |
| 5 | direction: 'refines', |
| 6 | correction: 'corrects', |
| 7 | 'scope-change': 'expands', |
| 8 | checkpoint: 'checkpoints', |
| 9 | question: 'asks', |
| 10 | rejection: 'rejects', |
| 11 | root: 'refines', |
| 12 | }; |
| 13 | |
| 14 | export function renderJson(tree, opts = {}) { |
| 15 | const { projectName, generatedBy = 'treetrace', version = '0.1.0', sourceType = 'claude-code-jsonl' } = opts; |
| 16 | const { nodes, sessions, stats } = tree; |
| 17 | const analysis = analyzeTree(tree); |
| 18 | |
| 19 | return { |
| 20 | schemaVersion: SCHEMA_VERSION, |
| 21 | generator: { name: generatedBy, version, url: REPO_URL }, |
| 22 | project: { |
| 23 | name: projectName, |
| 24 | generatedAt: opts.generatedAt || null, |
| 25 | sourceType, |
| 26 | }, |
| 27 | stats: { |
| 28 | prompts: stats.promptCount, |
| 29 | rawPrompts: stats.rawPromptCount, |
| 30 | sessions: stats.sessionCount, |
| 31 | days: stats.days, |
| 32 | corrections: stats.corrections, |
| 33 | scopeChanges: stats.scopeChanges, |
| 34 | checkpoints: stats.checkpoints, |
| 35 | abandonedBranches: stats.abandonedBranches, |
| 36 | rejections: stats.rejections || 0, |
| 37 | rejectionsByKind: stats.rejectionsByKind || {}, |
| 38 | toolUses: stats.toolUses, |
| 39 | filesTouched: stats.filesTouched, |
| 40 | inputTokens: stats.inputTokens || 0, |
| 41 | outputTokens: stats.outputTokens || 0, |
| 42 | models: stats.models, |
| 43 | firstTs: stats.firstTs, |
| 44 | lastTs: stats.lastTs, |
| 45 | }, |
| 46 | analysis: { |
| 47 | failureSignals: analysis.summary.totalFailureSignals, |
| 48 | correctionChains: analysis.summary.correctionChains, |
| 49 | evalCandidates: analysis.summary.evalCandidates, |
| 50 | lessons: analysis.summary.lessons, |
| 51 | }, |
| 52 | sessions: sessions |
| 53 | .filter((s) => s.prompts.length) |
| 54 | .map((s) => ({ |
| 55 | id: s.sessionId, |
| 56 | title: s.title, |
| 57 | firstTs: s.firstTs, |
| 58 | lastTs: s.lastTs, |
| 59 | promptCount: s.prompts.length, |
| 60 | isContinuation: s.isContinuation, |
| 61 | inputTokens: s.stats.inputTokens || 0, |
| 62 | outputTokens: s.stats.outputTokens || 0, |
| 63 | })), |
| 64 | nodes: nodes.map((n) => ({ |
| 65 | id: n.id, |
| 66 | parentId: n.parent ? n.parent.id : null, |
| 67 | role: 'user', |
| 68 | kind: n.kind, |
| 69 | title: n.title, |
| 70 | text: n.text, |
| 71 | status: n.status, |
| 72 | nudges: n.nudges || 0, |
| 73 | reruns: n.reruns || 0, |
| 74 | session: n.sessionId, |
| 75 | timestamp: n.ts, |
| 76 | model: n.model || null, |
| 77 | actions: (n.actions || []).map((a) => ({ |
| 78 | tool: a.tool || null, |
| 79 | file: a.file || null, |
| 80 | command: a.command || null, |
| 81 | model: a.model || null, |
| 82 | })), |
| 83 | failureSignals: n.failureSignals || [], |
| 84 | evalCandidate: Boolean(n.evalCandidate), |
| 85 | lessonIds: n.lessonIds || [], |
| 86 | rejections: n.rejections || [], |
| 87 | |
| 88 | sourceEventIds: n.uuid ? [n.uuid] : [], |
| 89 | })), |
| 90 | edges: nodes |
| 91 | .filter((n) => n.parent) |
| 92 | .map((n) => ({ |
| 93 | from: n.parent.id, |
| 94 | to: n.id, |
| 95 | relationship: RELATIONSHIP_BY_KIND[n.kind] || 'refines', |
| 96 | })), |
| 97 | correctionChains: analysis.correctionChains, |
| 98 | lessons: analysis.lessons, |
| 99 | evalCandidates: analysis.evalCandidates, |
| 100 | }; |
| 101 | } |