@limo-labs/limo-cli 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +238 -0
  2. package/dist/agents/analyst.d.ts +24 -0
  3. package/dist/agents/analyst.js +128 -0
  4. package/dist/agents/editor.d.ts +26 -0
  5. package/dist/agents/editor.js +157 -0
  6. package/dist/agents/planner-validator.d.ts +7 -0
  7. package/dist/agents/planner-validator.js +125 -0
  8. package/dist/agents/planner.d.ts +56 -0
  9. package/dist/agents/planner.js +186 -0
  10. package/dist/agents/writer.d.ts +25 -0
  11. package/dist/agents/writer.js +164 -0
  12. package/dist/commands/analyze.d.ts +14 -0
  13. package/dist/commands/analyze.js +562 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +41 -0
  16. package/dist/report/diagrams.d.ts +27 -0
  17. package/dist/report/diagrams.js +74 -0
  18. package/dist/report/graphCompiler.d.ts +37 -0
  19. package/dist/report/graphCompiler.js +277 -0
  20. package/dist/report/markdownGenerator.d.ts +71 -0
  21. package/dist/report/markdownGenerator.js +148 -0
  22. package/dist/tools/additional.d.ts +116 -0
  23. package/dist/tools/additional.js +349 -0
  24. package/dist/tools/extended.d.ts +101 -0
  25. package/dist/tools/extended.js +586 -0
  26. package/dist/tools/index.d.ts +86 -0
  27. package/dist/tools/index.js +362 -0
  28. package/dist/types/agents.types.d.ts +139 -0
  29. package/dist/types/agents.types.js +6 -0
  30. package/dist/types/graphSemantics.d.ts +99 -0
  31. package/dist/types/graphSemantics.js +104 -0
  32. package/dist/utils/debug.d.ts +28 -0
  33. package/dist/utils/debug.js +125 -0
  34. package/dist/utils/limoConfigParser.d.ts +21 -0
  35. package/dist/utils/limoConfigParser.js +274 -0
  36. package/dist/utils/reviewMonitor.d.ts +20 -0
  37. package/dist/utils/reviewMonitor.js +121 -0
  38. package/package.json +62 -0
  39. package/prompts/analyst.md +343 -0
  40. package/prompts/editor.md +196 -0
  41. package/prompts/planner.md +388 -0
  42. package/prompts/writer.md +218 -0
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Diagram rendering utilities for CLI reports
3
+ */
4
+ /**
5
+ * Embeds a diagram in Markdown format
6
+ *
7
+ * For SVG diagrams, embeds as base64 data URI image for universal compatibility.
8
+ * For DOT/Mermaid format, embeds as code blocks for external rendering.
9
+ */
10
+ export function embedDiagramInMarkdown(diagram) {
11
+ let markdown = '';
12
+ // Add title and description
13
+ if (diagram.title) {
14
+ markdown += `### ${diagram.title}\n\n`;
15
+ }
16
+ if (diagram.description) {
17
+ markdown += `${diagram.description}\n\n`;
18
+ }
19
+ // Embed diagram based on format
20
+ if (diagram.format === 'svg') {
21
+ // Convert SVG to base64 data URI and embed as image
22
+ const base64Svg = Buffer.from(diagram.source, 'utf-8').toString('base64');
23
+ const dataUri = `data:image/svg+xml;base64,${base64Svg}`;
24
+ const altText = diagram.title || 'Architecture Diagram';
25
+ markdown += `![${altText}](${dataUri})\n\n`;
26
+ }
27
+ else if (diagram.format === 'dot') {
28
+ // DOT as code block (requires external renderer)
29
+ markdown += `\`\`\`dot\n${diagram.source}\n\`\`\`\n\n`;
30
+ }
31
+ else if (diagram.format === 'mermaid') {
32
+ // Mermaid as code block
33
+ markdown += `\`\`\`mermaid\n${diagram.source}\n\`\`\`\n\n`;
34
+ }
35
+ else {
36
+ // Fallback for unknown formats
37
+ markdown += `\`\`\`${diagram.format}\n${diagram.source}\n\`\`\`\n\n`;
38
+ }
39
+ // Add metadata if present
40
+ if (diagram.metadata) {
41
+ if (diagram.metadata.nodeCount !== undefined || diagram.metadata.edgeCount !== undefined) {
42
+ markdown += `*Complexity: `;
43
+ const parts = [];
44
+ if (diagram.metadata.nodeCount !== undefined) {
45
+ parts.push(`${diagram.metadata.nodeCount} nodes`);
46
+ }
47
+ if (diagram.metadata.edgeCount !== undefined) {
48
+ parts.push(`${diagram.metadata.edgeCount} edges`);
49
+ }
50
+ markdown += parts.join(', ') + '*\n\n';
51
+ }
52
+ }
53
+ // Add related files if present
54
+ if (diagram.relatedFiles && diagram.relatedFiles.length > 0) {
55
+ markdown += `**Related files:**\n`;
56
+ for (const file of diagram.relatedFiles) {
57
+ // Use proper file path encoding
58
+ const encodedPath = encodeFilePathForMarkdown(file);
59
+ markdown += `- [\`${file}\`](${encodedPath})\n`;
60
+ }
61
+ markdown += '\n';
62
+ }
63
+ return markdown;
64
+ }
65
+ /**
66
+ * Encode file path for use in markdown links
67
+ * Preserves forward slashes for VS Code compatibility
68
+ */
69
+ function encodeFilePathForMarkdown(filePath) {
70
+ return filePath
71
+ .split('/')
72
+ .map(segment => encodeURIComponent(segment))
73
+ .join('/');
74
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Graph Semantics to SVG Compiler
3
+ *
4
+ * Converts graph semantic structure (IR) to SVG directly via DOT.
5
+ * This compiler uses ONLY explicit semantic fields from the IR.
6
+ * NO natural language inference, NO layout guessing.
7
+ *
8
+ * CRITICAL: LLM never sees this code. This is a pure code-side responsibility.
9
+ */
10
+ import type { GraphSemantics } from '../types/graphSemantics.js';
11
+ /**
12
+ * Compile graph semantics to DOT language
13
+ */
14
+ export declare function compileGraphToDot(semantics: GraphSemantics): string;
15
+ /**
16
+ * Generate DOT with validation
17
+ */
18
+ export declare function compileGraphToDotSafe(semantics: GraphSemantics): {
19
+ success: boolean;
20
+ dot?: string;
21
+ error?: string;
22
+ };
23
+ /**
24
+ * Compile graph semantics directly to SVG
25
+ *
26
+ * This is the primary function for diagram generation.
27
+ * It compiles semantic IR to DOT, then uses Viz.js to render SVG.
28
+ */
29
+ export declare function compileGraphToSvg(semantics: GraphSemantics): Promise<string>;
30
+ /**
31
+ * Compile graph semantics to SVG with validation
32
+ */
33
+ export declare function compileGraphToSvgSafe(semantics: GraphSemantics): Promise<{
34
+ success: boolean;
35
+ svg?: string;
36
+ error?: string;
37
+ }>;
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Graph Semantics to SVG Compiler
3
+ *
4
+ * Converts graph semantic structure (IR) to SVG directly via DOT.
5
+ * This compiler uses ONLY explicit semantic fields from the IR.
6
+ * NO natural language inference, NO layout guessing.
7
+ *
8
+ * CRITICAL: LLM never sees this code. This is a pure code-side responsibility.
9
+ */
10
+ import { instance } from '@viz-js/viz';
11
+ /**
12
+ * Cached Viz.js instance for performance
13
+ */
14
+ let vizInstance = null;
15
+ /**
16
+ * Get or initialize Viz.js instance
17
+ */
18
+ async function getVizInstance() {
19
+ if (!vizInstance) {
20
+ vizInstance = await instance();
21
+ }
22
+ return vizInstance;
23
+ }
24
+ /**
25
+ * Node shape mapping based on semantic type
26
+ */
27
+ const NODE_SHAPES = {
28
+ 'component': 'box',
29
+ 'layer': 'folder',
30
+ 'system': 'hexagon',
31
+ 'data': 'cylinder',
32
+ 'process': 'ellipse',
33
+ 'actor': 'house'
34
+ };
35
+ /**
36
+ * Node color mapping based on semantic type
37
+ */
38
+ const NODE_COLORS = {
39
+ 'component': { fillcolor: '#E3F2FD', fontcolor: '#0D47A1' },
40
+ 'layer': { fillcolor: '#F3E5F5', fontcolor: '#4A148C' },
41
+ 'system': { fillcolor: '#FFF9C4', fontcolor: '#F57F17' },
42
+ 'data': { fillcolor: '#E0F2F1', fontcolor: '#004D40' },
43
+ 'process': { fillcolor: '#FCE4EC', fontcolor: '#880E4F' },
44
+ 'actor': { fillcolor: '#E8F5E9', fontcolor: '#1B5E20' }
45
+ };
46
+ /**
47
+ * Edge style mapping based on semantic type
48
+ */
49
+ const EDGE_STYLES = {
50
+ 'depends_on': { style: 'solid', arrowhead: 'normal', color: '#424242' },
51
+ 'calls': { style: 'solid', arrowhead: 'vee', color: '#1976D2' },
52
+ 'uses': { style: 'dashed', arrowhead: 'open', color: '#424242' },
53
+ 'contains': { style: 'solid', arrowhead: 'diamond', color: '#4CAF50' },
54
+ 'inherits': { style: 'solid', arrowhead: 'empty', color: '#9C27B0' },
55
+ 'implements': { style: 'dashed', arrowhead: 'empty', color: '#9C27B0' },
56
+ 'data_flow': { style: 'bold', arrowhead: 'normal', color: '#FF9800' },
57
+ 'control_flow': { style: 'bold', arrowhead: 'normal', color: '#F44336' }
58
+ };
59
+ /**
60
+ * Compile graph semantics to DOT language
61
+ */
62
+ export function compileGraphToDot(semantics) {
63
+ const lines = [];
64
+ const indent = ' ';
65
+ // Start digraph
66
+ lines.push(`digraph ${quote(sanitizeId(semantics.id))} {`);
67
+ // Graph attributes
68
+ const direction = semantics.metadata?.direction || 'TB';
69
+ lines.push(`${indent}rankdir="${direction}";`);
70
+ lines.push(`${indent}bgcolor="transparent";`);
71
+ lines.push(`${indent}fontname="Arial";`);
72
+ lines.push(`${indent}fontsize=12;`);
73
+ lines.push(`${indent}nodesep=0.5;`);
74
+ lines.push(`${indent}ranksep=0.75;`);
75
+ lines.push('');
76
+ // Default node attributes
77
+ lines.push(`${indent}node [style=filled, fontname="Arial", fontsize=11, margin="0.2,0.1"];`);
78
+ lines.push('');
79
+ // Default edge attributes
80
+ lines.push(`${indent}edge [fontname="Arial", fontsize=9];`);
81
+ lines.push('');
82
+ // Build group structure
83
+ const groups = semantics.groups || [];
84
+ const groupsByParent = organizeGroupsByParent(groups);
85
+ const nodesInGroups = new Set();
86
+ // Add groups and their nodes
87
+ if (groups.length > 0) {
88
+ addGroupsRecursive(lines, indent, groups, groupsByParent, semantics.nodes, nodesInGroups, semantics.metadata?.focusNodes);
89
+ }
90
+ // Add ungrouped nodes
91
+ for (const node of semantics.nodes) {
92
+ if (!node.group || !nodesInGroups.has(node.id)) {
93
+ addNodeDef(lines, indent, node, semantics.metadata?.focusNodes);
94
+ }
95
+ }
96
+ // Add edges
97
+ if (semantics.edges.length > 0) {
98
+ lines.push('');
99
+ for (const edge of semantics.edges) {
100
+ addEdgeDef(lines, indent, edge);
101
+ }
102
+ }
103
+ // Close digraph
104
+ lines.push('}');
105
+ return lines.join('\n');
106
+ }
107
+ /**
108
+ * Organize groups by parent
109
+ */
110
+ function organizeGroupsByParent(groups) {
111
+ const map = new Map();
112
+ for (const group of groups) {
113
+ const parent = group.parent;
114
+ if (!map.has(parent)) {
115
+ map.set(parent, []);
116
+ }
117
+ map.get(parent).push(group);
118
+ }
119
+ return map;
120
+ }
121
+ /**
122
+ * Add groups recursively with their nodes
123
+ */
124
+ function addGroupsRecursive(lines, baseIndent, allGroups, groupsByParent, nodes, nodesInGroups, focusNodes, parentId, currentDepth = 0) {
125
+ const children = groupsByParent.get(parentId) || [];
126
+ const indent = baseIndent.repeat(currentDepth + 1);
127
+ for (const group of children) {
128
+ // Start subgraph
129
+ lines.push('');
130
+ lines.push(`${indent}subgraph cluster_${sanitizeId(group.id)} {`);
131
+ lines.push(`${indent} label=${quote(group.label)};`);
132
+ lines.push(`${indent} style="rounded,filled";`);
133
+ lines.push(`${indent} fillcolor="#F5F5F5";`);
134
+ lines.push(`${indent} color="#BDBDBD";`);
135
+ lines.push(`${indent} fontsize=12;`);
136
+ lines.push(`${indent} fontname="Arial Bold";`);
137
+ if (group.description) {
138
+ lines.push(`${indent} tooltip=${quote(group.description)};`);
139
+ }
140
+ // Add nodes in this group
141
+ for (const node of nodes) {
142
+ if (node.group === group.id) {
143
+ addNodeDef(lines, indent + ' ', node, focusNodes);
144
+ nodesInGroups.add(node.id);
145
+ }
146
+ }
147
+ // Recursively add child groups
148
+ addGroupsRecursive(lines, baseIndent, allGroups, groupsByParent, nodes, nodesInGroups, focusNodes, group.id, currentDepth + 1);
149
+ // End subgraph
150
+ lines.push(`${indent}}`);
151
+ }
152
+ }
153
+ /**
154
+ * Add node definition
155
+ */
156
+ function addNodeDef(lines, indent, node, focusNodes) {
157
+ const shape = NODE_SHAPES[node.type] || 'box';
158
+ const colors = NODE_COLORS[node.type] || { fillcolor: '#FFFFFF', fontcolor: '#000000' };
159
+ const isFocused = focusNodes?.includes(node.id);
160
+ const attrs = [
161
+ `label=${quote(node.label)}`,
162
+ `shape=${shape}`,
163
+ `fillcolor="${colors.fillcolor}"`,
164
+ `fontcolor="${colors.fontcolor}"`,
165
+ `style="${isFocused ? 'filled,bold' : 'filled'}"`,
166
+ `penwidth=${isFocused ? '2' : '1'}`,
167
+ `tooltip=${quote(node.description || node.label)}`
168
+ ];
169
+ lines.push(`${indent}${sanitizeId(node.id)} [${attrs.join(', ')}];`);
170
+ }
171
+ /**
172
+ * Add edge definition
173
+ */
174
+ function addEdgeDef(lines, indent, edge) {
175
+ const style = EDGE_STYLES[edge.type] || { style: 'solid', arrowhead: 'normal', color: '#424242' };
176
+ const attrs = [
177
+ `style=${style.style}`,
178
+ `arrowhead=${style.arrowhead}`,
179
+ `color="${style.color}"`
180
+ ];
181
+ if (edge.label) {
182
+ attrs.push(`label=${quote(edge.label)}`);
183
+ }
184
+ if (edge.metadata?.weight) {
185
+ const penwidth = Math.max(1, Math.min(5, edge.metadata.weight / 2));
186
+ attrs.push(`penwidth=${penwidth}`);
187
+ }
188
+ if (edge.bidirectional) {
189
+ attrs.push('dir=both');
190
+ }
191
+ const arrow = edge.bidirectional ? '--' : '->';
192
+ lines.push(`${indent}${sanitizeId(edge.from)} ${arrow} ${sanitizeId(edge.to)} [${attrs.join(', ')}];`);
193
+ }
194
+ /**
195
+ * DOT reserved keywords that cannot be used as node IDs
196
+ */
197
+ const DOT_KEYWORDS = new Set([
198
+ 'node', 'edge', 'graph', 'digraph', 'subgraph', 'strict',
199
+ // Also include common DOT attribute names to avoid confusion
200
+ 'label', 'shape', 'color', 'style', 'rank'
201
+ ]);
202
+ /**
203
+ * Sanitize ID to be valid DOT identifier
204
+ *
205
+ * Handles:
206
+ * - Special characters (replaced with underscores)
207
+ * - IDs starting with digits (prefixed with 'n_')
208
+ * - DOT reserved keywords (prefixed with 'n_')
209
+ * - Empty IDs (replaced with random ID)
210
+ */
211
+ function sanitizeId(id) {
212
+ // 1. Replace invalid characters with underscores
213
+ let cleaned = id.replace(/[^a-zA-Z0-9_]/g, '_');
214
+ // 2. Handle IDs starting with digits (DOT doesn't allow this)
215
+ if (/^[0-9]/.test(cleaned)) {
216
+ cleaned = 'n_' + cleaned;
217
+ }
218
+ // 3. Handle DOT reserved keywords (case-insensitive)
219
+ if (DOT_KEYWORDS.has(cleaned.toLowerCase())) {
220
+ cleaned = 'n_' + cleaned;
221
+ }
222
+ // 4. Ensure non-empty ID
223
+ if (cleaned === '' || cleaned === '_') {
224
+ cleaned = 'node_' + Math.random().toString(36).substring(7);
225
+ }
226
+ return cleaned;
227
+ }
228
+ /**
229
+ * Quote string for DOT
230
+ */
231
+ function quote(str) {
232
+ return '"' + str.replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
233
+ }
234
+ /**
235
+ * Generate DOT with validation
236
+ */
237
+ export function compileGraphToDotSafe(semantics) {
238
+ try {
239
+ const dot = compileGraphToDot(semantics);
240
+ return { success: true, dot };
241
+ }
242
+ catch (error) {
243
+ return {
244
+ success: false,
245
+ error: `Failed to compile graph to DOT: ${error instanceof Error ? error.message : String(error)}`
246
+ };
247
+ }
248
+ }
249
+ /**
250
+ * Compile graph semantics directly to SVG
251
+ *
252
+ * This is the primary function for diagram generation.
253
+ * It compiles semantic IR to DOT, then uses Viz.js to render SVG.
254
+ */
255
+ export async function compileGraphToSvg(semantics) {
256
+ // Step 1: Compile semantic structure to DOT
257
+ const dot = compileGraphToDot(semantics);
258
+ // Step 2: Render DOT to SVG using Viz.js
259
+ const viz = await getVizInstance();
260
+ const svg = viz.renderString(dot, { format: 'svg', engine: 'dot' });
261
+ return svg;
262
+ }
263
+ /**
264
+ * Compile graph semantics to SVG with validation
265
+ */
266
+ export async function compileGraphToSvgSafe(semantics) {
267
+ try {
268
+ const svg = await compileGraphToSvg(semantics);
269
+ return { success: true, svg };
270
+ }
271
+ catch (error) {
272
+ return {
273
+ success: false,
274
+ error: `Failed to compile graph to SVG: ${error instanceof Error ? error.message : String(error)}`
275
+ };
276
+ }
277
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Markdown Report Generator
3
+ *
4
+ * Generates professional markdown reports with embedded diagrams,
5
+ * rich metadata, and statistics - matching VSCode report quality.
6
+ */
7
+ import type { DiagramDefinition } from './diagrams.js';
8
+ export interface ReportMetadata {
9
+ id: string;
10
+ title: string;
11
+ generatedAt: Date;
12
+ projectPath: string;
13
+ aiModel?: string;
14
+ modules: string[];
15
+ statistics?: {
16
+ totalSections?: number;
17
+ totalDiagrams?: number;
18
+ totalFindings?: number;
19
+ filesAnalyzed?: number;
20
+ };
21
+ }
22
+ export interface ReportSection {
23
+ id: string;
24
+ title: string;
25
+ content: string;
26
+ diagrams?: DiagramDefinition[];
27
+ }
28
+ export interface Report {
29
+ metadata: ReportMetadata;
30
+ executiveSummary?: string;
31
+ sections: ReportSection[];
32
+ }
33
+ export declare class MarkdownGenerator {
34
+ /**
35
+ * Generate a complete markdown report
36
+ */
37
+ generate(report: Report): string;
38
+ /**
39
+ * Generate report header with rich metadata
40
+ */
41
+ private generateHeader;
42
+ /**
43
+ * Generate executive summary section
44
+ */
45
+ private generateExecutiveSummary;
46
+ /**
47
+ * Generate a single section with embedded diagrams
48
+ */
49
+ private generateSection;
50
+ /**
51
+ * Embed a single diagram
52
+ */
53
+ private embedDiagram;
54
+ /**
55
+ * Format date in a human-readable way
56
+ */
57
+ private formatDate;
58
+ }
59
+ /**
60
+ * Helper function to extract title from markdown content
61
+ */
62
+ export declare function extractTitle(content: string): string;
63
+ /**
64
+ * Helper function to count findings in content
65
+ * Counts bullet points, numbered items, and sections
66
+ */
67
+ export declare function countFindings(content: string): number;
68
+ /**
69
+ * Helper function to count findings across all sections
70
+ */
71
+ export declare function countAllFindings(sections: ReportSection[]): number;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Markdown Report Generator
3
+ *
4
+ * Generates professional markdown reports with embedded diagrams,
5
+ * rich metadata, and statistics - matching VSCode report quality.
6
+ */
7
+ import { embedDiagramInMarkdown } from './diagrams.js';
8
+ export class MarkdownGenerator {
9
+ /**
10
+ * Generate a complete markdown report
11
+ */
12
+ generate(report) {
13
+ const parts = [];
14
+ // 1. Header with metadata
15
+ parts.push(this.generateHeader(report.metadata));
16
+ // 2. Executive Summary (if available)
17
+ if (report.executiveSummary) {
18
+ parts.push(this.generateExecutiveSummary(report.executiveSummary));
19
+ }
20
+ // 3. Sections with embedded diagrams
21
+ for (const section of report.sections) {
22
+ parts.push(this.generateSection(section));
23
+ }
24
+ return parts.join('\n\n');
25
+ }
26
+ /**
27
+ * Generate report header with rich metadata
28
+ */
29
+ generateHeader(metadata) {
30
+ let header = `# ${metadata.title}\n\n`;
31
+ // Basic metadata
32
+ header += `**Generated**: ${this.formatDate(metadata.generatedAt)}\n`;
33
+ header += `**Project**: ${metadata.projectPath}\n`;
34
+ // AI Model (if available)
35
+ if (metadata.aiModel) {
36
+ header += `**AI Model**: ${metadata.aiModel}\n`;
37
+ }
38
+ // Analysis Modules
39
+ if (metadata.modules.length > 0) {
40
+ header += `**Analysis Modules**: ${metadata.modules.join(', ')}\n`;
41
+ }
42
+ // Statistics (if available)
43
+ if (metadata.statistics) {
44
+ header += '\n**Statistics**:\n';
45
+ const stats = metadata.statistics;
46
+ if (stats.totalSections !== undefined) {
47
+ header += `- Report Sections: ${stats.totalSections}\n`;
48
+ }
49
+ if (stats.totalDiagrams !== undefined) {
50
+ header += `- Diagrams: ${stats.totalDiagrams}\n`;
51
+ }
52
+ if (stats.totalFindings !== undefined) {
53
+ header += `- Key Findings: ${stats.totalFindings}\n`;
54
+ }
55
+ if (stats.filesAnalyzed !== undefined) {
56
+ header += `- Files Analyzed: ${stats.filesAnalyzed}\n`;
57
+ }
58
+ }
59
+ header += '\n---';
60
+ return header;
61
+ }
62
+ /**
63
+ * Generate executive summary section
64
+ */
65
+ generateExecutiveSummary(summary) {
66
+ // Check if summary already has markdown formatting
67
+ // If it already starts with "##", use it as-is
68
+ if (summary.trim().startsWith('##')) {
69
+ return `${summary}\n\n---`;
70
+ }
71
+ // Otherwise, wrap it with a heading
72
+ return `## 📊 Executive Summary\n\n${summary}\n\n---`;
73
+ }
74
+ /**
75
+ * Generate a single section with embedded diagrams
76
+ */
77
+ generateSection(section) {
78
+ let markdown = section.content;
79
+ // Embed diagrams within the section (not at the end!)
80
+ if (section.diagrams && section.diagrams.length > 0) {
81
+ // Add diagrams at the end of the section content
82
+ markdown += '\n\n';
83
+ for (const diagram of section.diagrams) {
84
+ markdown += this.embedDiagram(diagram) + '\n\n';
85
+ }
86
+ }
87
+ return markdown.trim();
88
+ }
89
+ /**
90
+ * Embed a single diagram
91
+ */
92
+ embedDiagram(diagram) {
93
+ return embedDiagramInMarkdown(diagram);
94
+ }
95
+ /**
96
+ * Format date in a human-readable way
97
+ */
98
+ formatDate(date) {
99
+ return date.toLocaleString('en-US', {
100
+ year: 'numeric',
101
+ month: 'long',
102
+ day: 'numeric',
103
+ hour: '2-digit',
104
+ minute: '2-digit',
105
+ second: '2-digit',
106
+ hour12: false
107
+ });
108
+ }
109
+ }
110
+ /**
111
+ * Helper function to extract title from markdown content
112
+ */
113
+ export function extractTitle(content) {
114
+ // Find first ## heading
115
+ const match = content.match(/^##\s+(.+)$/m);
116
+ if (match) {
117
+ return match[1].trim();
118
+ }
119
+ // Fallback: use first non-empty line
120
+ const firstLine = content.split('\n').find(line => line.trim().length > 0);
121
+ return firstLine?.trim() || 'Untitled Section';
122
+ }
123
+ /**
124
+ * Helper function to count findings in content
125
+ * Counts bullet points, numbered items, and sections
126
+ */
127
+ export function countFindings(content) {
128
+ let count = 0;
129
+ // Count bullet points (- or *)
130
+ const bulletPoints = content.match(/^[\s]*[-*]\s+/gm);
131
+ if (bulletPoints) {
132
+ count += bulletPoints.length;
133
+ }
134
+ // Count numbered items (1., 2., etc.)
135
+ const numberedItems = content.match(/^[\s]*\d+\.\s+/gm);
136
+ if (numberedItems) {
137
+ count += numberedItems.length;
138
+ }
139
+ return count;
140
+ }
141
+ /**
142
+ * Helper function to count findings across all sections
143
+ */
144
+ export function countAllFindings(sections) {
145
+ return sections.reduce((total, section) => {
146
+ return total + countFindings(section.content);
147
+ }, 0);
148
+ }