task-summary-extractor 8.1.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.
@@ -0,0 +1,567 @@
1
+ /**
2
+ * Dynamic Mode — AI-powered document generation from video + documents + user request.
3
+ *
4
+ * This mode automatically detects and processes ALL content in a folder:
5
+ * - Video files: compressed, segmented, analyzed for content summaries
6
+ * - Documents: loaded as text context
7
+ *
8
+ * The pipeline then:
9
+ * 1. Discovers all videos and documents
10
+ * 2. Compresses and segments videos, analyzes each segment
11
+ * 3. Sends video summaries + docs + request to Gemini for topic planning
12
+ * 4. Generates a set of Markdown documents — one per topic
13
+ *
14
+ * Works with any type of video (mp4, mkv, avi, mov, webm) and any documents.
15
+ *
16
+ * Fully backward-compatible — works with docs-only folders too.
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const config = require('../config');
24
+ // Access config.GEMINI_MODEL at call time (not destructured) for runtime model changes.
25
+ const { extractJson } = require('./json-parser');
26
+ const { withRetry } = require('./retry');
27
+
28
+ // ======================== TOPIC PLANNING ========================
29
+
30
+ /**
31
+ * Ask Gemini to plan a set of documents based on video + docs + user request.
32
+ *
33
+ * @param {object} ai - GoogleGenAI instance
34
+ * @param {string} userRequest - What the user wants generated
35
+ * @param {string[]} docSnippets - Content from context documents
36
+ * @param {object} options
37
+ * @param {string} [options.folderName] - Name of the source folder
38
+ * @param {string} [options.userName] - User's name
39
+ * @param {number} [options.thinkingBudget] - Thinking tokens
40
+ * @param {Array} [options.videoSummaries] - Video segment summaries from analyzeVideoForContext
41
+ * @returns {Promise<{topics: Array, raw: string, durationMs: number, tokenUsage: object}>}
42
+ */
43
+ async function planTopics(ai, userRequest, docSnippets, options = {}) {
44
+ const { folderName = 'project', userName = '', thinkingBudget = 16384, videoSummaries = [] } = options;
45
+
46
+ // Build video context section
47
+ const videoSection = videoSummaries.length > 0
48
+ ? `\n\nVIDEO CONTENT ANALYZED (${videoSummaries.length} segment${videoSummaries.length > 1 ? 's' : ''}):\n` +
49
+ videoSummaries.map((vs, i) => {
50
+ const label = vs.totalSegments > 1
51
+ ? `--- Video: ${vs.videoFile} — Segment ${vs.segmentIndex + 1}/${vs.totalSegments} ---`
52
+ : `--- Video: ${vs.videoFile} ---`;
53
+ return `${label}\n${vs.summary}`;
54
+ }).join('\n\n')
55
+ : '';
56
+
57
+ const docsSection = docSnippets.length > 0
58
+ ? `\n\nCONTEXT DOCUMENTS PROVIDED:\n${docSnippets.join('\n\n---\n\n')}`
59
+ : '';
60
+
61
+ const hasVideo = videoSummaries.length > 0;
62
+ const hasDocs = docSnippets.length > 0;
63
+ const sourceDescription = hasVideo && hasDocs
64
+ ? '(Video recordings + context documents provided — use BOTH as source material)'
65
+ : hasVideo
66
+ ? '(Video recordings provided — use the video content as your primary source material)'
67
+ : hasDocs
68
+ ? '(Context documents provided — use them as source material)'
69
+ : '(No context provided — generate based on the request alone)';
70
+
71
+ const prompt = `You are an expert knowledge architect and technical writer. A user has a request and optionally provided context — which may include video recordings, documents, or both. Your job is to plan a set of Markdown documents that fully address their request.
72
+
73
+ USER REQUEST:
74
+ "${userRequest}"
75
+
76
+ SOURCE FOLDER: "${folderName}"
77
+ ${userName ? `USER: "${userName}"` : ''}
78
+ ${sourceDescription}
79
+ ${videoSection}
80
+ ${docsSection}
81
+
82
+ YOUR TASK:
83
+ Plan 3-15 standalone Markdown documents that together comprehensively address the user's request. Each document should focus on ONE aspect/topic.
84
+
85
+ DOCUMENT CATEGORIES (use these exact names):
86
+ - "overview" — High-level summaries, executive briefs, introductions
87
+ - "guide" — How-to guides, step-by-step instructions, tutorials
88
+ - "analysis" — Analysis documents, comparisons, evaluations, assessments
89
+ - "plan" — Plans, roadmaps, timelines, strategies, proposals
90
+ - "reference" — Reference material, specifications, API docs, schemas
91
+ - "concept" — Concept explanations, definitions, theory, background
92
+ - "decision" — Decision records, options analysis, trade-off evaluations
93
+ - "checklist" — Checklists, verification lists, audit documents
94
+ - "template" — Templates, scaffolds, reusable patterns
95
+ - "report" — Status reports, summaries, findings
96
+
97
+ RULES:
98
+ 1. Plan 3-15 documents. More for complex requests, fewer for simple ones.
99
+ 2. Each document should be substantial (200-1000+ words depending on complexity).
100
+ 3. Documents should be self-contained but reference each other where relevant.
101
+ 4. First document should always be an overview/index of the entire set.
102
+ 5. Order by logical reading sequence — overview first, then foundational, then detailed.
103
+ 6. Each topic should have clear value — don't pad with trivial docs.
104
+ 7. Use ALL provided context (video content + documents) to ground your planning in reality.
105
+ 8. If video recordings are provided, extract insights, discussions, decisions, and details from them.
106
+ 9. If the request is about learning/teaching, include progressive complexity.
107
+ 10. If the request is about planning/migration, include risk analysis and timelines.
108
+ 11. Be creative but practical — generate what would actually help someone.
109
+
110
+ RESPOND WITH ONLY VALID JSON — no markdown fences, no extra text:
111
+
112
+ {
113
+ "topics": [
114
+ {
115
+ "id": "DM-01",
116
+ "title": "Clear document title",
117
+ "category": "overview|guide|analysis|plan|reference|concept|decision|checklist|template|report",
118
+ "description": "2-3 sentence description of what this document covers",
119
+ "target_audience": "Who this document is for",
120
+ "estimated_length": "short|medium|long",
121
+ "depends_on": []
122
+ }
123
+ ],
124
+ "project_summary": "One-line summary of the document set's purpose"
125
+ }`;
126
+
127
+ const requestPayload = {
128
+ model: config.GEMINI_MODEL,
129
+ contents: [{ role: 'user', parts: [{ text: prompt }] }],
130
+ config: {
131
+ systemInstruction: 'You are a knowledge architect. Plan a comprehensive set of documents based on the user\'s request and provided context. Respond with valid JSON only.',
132
+ maxOutputTokens: 16384,
133
+ temperature: 0.3,
134
+ thinkingConfig: { thinkingBudget },
135
+ },
136
+ };
137
+
138
+ const t0 = Date.now();
139
+ const response = await withRetry(
140
+ () => ai.models.generateContent(requestPayload),
141
+ { label: 'Dynamic mode topic planning', maxRetries: 2, baseDelay: 3000 }
142
+ );
143
+ const durationMs = Date.now() - t0;
144
+ const rawText = response.text;
145
+
146
+ const parsed = extractJson(rawText);
147
+ const topics = parsed?.topics || [];
148
+ const projectSummary = parsed?.project_summary || '';
149
+
150
+ const usage = response.usageMetadata || {};
151
+ const tokenUsage = {
152
+ inputTokens: usage.promptTokenCount || 0,
153
+ outputTokens: usage.candidatesTokenCount || 0,
154
+ totalTokens: usage.totalTokenCount || 0,
155
+ thoughtTokens: usage.thoughtsTokenCount || 0,
156
+ };
157
+
158
+ return { topics, projectSummary, raw: rawText, durationMs, tokenUsage };
159
+ }
160
+
161
+ // ======================== DOCUMENT GENERATION ========================
162
+
163
+ /**
164
+ * Generate a single document for a planned topic.
165
+ *
166
+ * @param {object} ai - GoogleGenAI instance
167
+ * @param {object} topic - Topic from planTopics
168
+ * @param {string} userRequest - Original user request
169
+ * @param {string[]} docSnippets - Context document content
170
+ * @param {object} options
171
+ * @param {Array} [options.videoSummaries] - Video segment summaries
172
+ * @returns {Promise<{markdown: string, raw: string, durationMs: number, tokenUsage: object}>}
173
+ */
174
+ async function generateDynamicDocument(ai, topic, userRequest, docSnippets, options = {}) {
175
+ const { folderName = 'project', userName = '', thinkingBudget = 16384, allTopics = [], videoSummaries = [] } = options;
176
+
177
+ // Build the list of related documents for cross-references
178
+ const otherDocs = allTopics
179
+ .filter(t => t.id !== topic.id)
180
+ .map(t => `- ${t.id}: ${t.title} (${t.category})`)
181
+ .join('\n');
182
+
183
+ const contextSection = docSnippets.length > 0
184
+ ? `\nCONTEXT DOCUMENTS:\n${docSnippets.slice(0, 5).join('\n---\n')}`
185
+ : '';
186
+
187
+ // Build video context for this document
188
+ const videoContextSection = videoSummaries.length > 0
189
+ ? `\nVIDEO CONTENT (from ${videoSummaries.length} analyzed segment${videoSummaries.length > 1 ? 's' : ''}):\n` +
190
+ videoSummaries.map((vs, i) => {
191
+ const label = vs.totalSegments > 1
192
+ ? `--- ${vs.videoFile} — Segment ${vs.segmentIndex + 1}/${vs.totalSegments} ---`
193
+ : `--- ${vs.videoFile} ---`;
194
+ // Truncate very long summaries per segment to manage token budget
195
+ const summary = vs.summary.length > 6000
196
+ ? vs.summary.slice(0, 6000) + '\n... (truncated for token budget)'
197
+ : vs.summary;
198
+ return `${label}\n${summary}`;
199
+ }).join('\n\n')
200
+ : '';
201
+
202
+ const categoryGuidance = getDynamicCategoryGuidance(topic.category);
203
+
204
+ // Adaptive max tokens based on estimated length
205
+ const maxOutputTokens = topic.estimated_length === 'long' ? 16384
206
+ : topic.estimated_length === 'medium' ? 8192
207
+ : 4096;
208
+
209
+ const prompt = `You are an expert technical writer creating a document as part of a comprehensive document set.
210
+
211
+ USER'S ORIGINAL REQUEST:
212
+ "${userRequest}"
213
+
214
+ DOCUMENT TO WRITE:
215
+ - ID: ${topic.id}
216
+ - Title: "${topic.title}"
217
+ - Category: ${topic.category}
218
+ - Description: ${topic.description}
219
+ - Target Audience: ${topic.target_audience || 'General'}
220
+
221
+ OTHER DOCUMENTS IN THE SET (for cross-references):
222
+ ${otherDocs || '(This is the only document)'}
223
+ ${videoContextSection}
224
+ ${contextSection}
225
+
226
+ ${categoryGuidance}
227
+
228
+ WRITING RULES:
229
+ 1. Write in clear, professional Markdown.
230
+ 2. Use headers (##, ###), bullet points, tables, code blocks, and diagrams where helpful.
231
+ 3. Target ${topic.estimated_length === 'long' ? '800-1500' : topic.estimated_length === 'medium' ? '400-800' : '200-400'} words.
232
+ 4. Write for the specified target audience — adjust technical depth accordingly.
233
+ 5. Reference other documents in the set using their titles where relevant (e.g., "See [Document Title]").
234
+ 6. Ground content in ALL provided context — video recordings AND documents when available.
235
+ 7. If video content is provided, use specific details, quotes, and decisions from the video.
236
+ 8. Be practical and actionable — include concrete examples, steps, or recommendations.
237
+ 9. DO NOT include YAML frontmatter or metadata blocks.
238
+ 10. Start with a level-1 heading (# Title) followed by a brief introduction.
239
+ 11. Include a "Summary" or "Key Takeaways" section at the end for longer docs.
240
+
241
+ START YOUR RESPONSE DIRECTLY WITH THE MARKDOWN CONTENT (no fences, no preamble):`;
242
+
243
+ const requestPayload = {
244
+ model: config.GEMINI_MODEL,
245
+ contents: [{ role: 'user', parts: [{ text: prompt }] }],
246
+ config: {
247
+ systemInstruction: 'You are a technical writer creating comprehensive documentation. Write clear, well-structured Markdown that directly addresses the request. Start directly with the content.',
248
+ maxOutputTokens,
249
+ temperature: 0.4,
250
+ thinkingConfig: { thinkingBudget },
251
+ },
252
+ };
253
+
254
+ const t0 = Date.now();
255
+ const response = await withRetry(
256
+ () => ai.models.generateContent(requestPayload),
257
+ { label: `Dynamic doc: ${topic.title}`, maxRetries: 2, baseDelay: 3000 }
258
+ );
259
+ const durationMs = Date.now() - t0;
260
+ const rawText = response.text;
261
+
262
+ // Clean up markdown fences if model wrapped output
263
+ let markdown = rawText.trim();
264
+ if (markdown.startsWith('```markdown')) {
265
+ markdown = markdown.replace(/^```markdown\s*\n?/, '').replace(/\n?```\s*$/, '');
266
+ } else if (markdown.startsWith('```md')) {
267
+ markdown = markdown.replace(/^```md\s*\n?/, '').replace(/\n?```\s*$/, '');
268
+ } else if (markdown.startsWith('```')) {
269
+ markdown = markdown.replace(/^```\s*\n?/, '').replace(/\n?```\s*$/, '');
270
+ }
271
+
272
+ const usage = response.usageMetadata || {};
273
+ const tokenUsage = {
274
+ inputTokens: usage.promptTokenCount || 0,
275
+ outputTokens: usage.candidatesTokenCount || 0,
276
+ totalTokens: usage.totalTokenCount || 0,
277
+ thoughtTokens: usage.thoughtsTokenCount || 0,
278
+ };
279
+
280
+ return { markdown, raw: rawText, durationMs, tokenUsage };
281
+ }
282
+
283
+ // ======================== BATCH GENERATION ========================
284
+
285
+ /**
286
+ * Generate all planned documents in parallel batches.
287
+ */
288
+ async function generateAllDynamicDocuments(ai, topics, userRequest, docSnippets, options = {}) {
289
+ const { concurrency = 2, onProgress, ...docOptions } = options;
290
+
291
+ const results = [];
292
+ const queue = [...topics];
293
+ let completed = 0;
294
+
295
+ while (queue.length > 0) {
296
+ const batch = queue.splice(0, concurrency);
297
+ const batchResults = await Promise.allSettled(
298
+ batch.map(topic =>
299
+ generateDynamicDocument(ai, topic, userRequest, docSnippets, { ...docOptions, allTopics: topics })
300
+ .then(result => {
301
+ completed++;
302
+ if (onProgress) onProgress(completed, topics.length, topic);
303
+ return { topic, ...result };
304
+ })
305
+ )
306
+ );
307
+
308
+ for (let i = 0; i < batchResults.length; i++) {
309
+ const r = batchResults[i];
310
+ if (r.status === 'fulfilled') {
311
+ results.push(r.value);
312
+ } else {
313
+ completed++;
314
+ if (onProgress) onProgress(completed, topics.length, batch[i]);
315
+ results.push({
316
+ topic: batch[i],
317
+ markdown: null,
318
+ raw: null,
319
+ durationMs: 0,
320
+ tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, thoughtTokens: 0 },
321
+ error: r.reason?.message || 'Unknown error',
322
+ });
323
+ }
324
+ }
325
+ }
326
+
327
+ return results;
328
+ }
329
+
330
+ // ======================== OUTPUT ========================
331
+
332
+ /**
333
+ * Write all dynamic documents to disk with an index.
334
+ *
335
+ * @param {string} outputDir - Directory to write to
336
+ * @param {Array} documents - Results from generateAllDynamicDocuments
337
+ * @param {object} meta - Metadata
338
+ * @returns {{ indexPath: string, docPaths: string[], stats: object }}
339
+ */
340
+ function writeDynamicOutput(outputDir, documents, meta = {}) {
341
+ fs.mkdirSync(outputDir, { recursive: true });
342
+
343
+ const docPaths = [];
344
+ const successful = documents.filter(d => d.markdown);
345
+ const failed = documents.filter(d => !d.markdown);
346
+
347
+ // Write individual documents
348
+ for (const doc of successful) {
349
+ const slug = slugify(doc.topic.title);
350
+ const fileName = `${doc.topic.id.toLowerCase()}-${slug}.md`;
351
+ const filePath = path.join(outputDir, fileName);
352
+ fs.writeFileSync(filePath, doc.markdown, 'utf8');
353
+ docPaths.push(filePath);
354
+ doc._fileName = fileName;
355
+ }
356
+
357
+ // Build index
358
+ const indexLines = [
359
+ `# ${meta.projectSummary || meta.userRequest || 'Generated Documents'}`,
360
+ '',
361
+ `> Generated from: **${meta.folderName || 'project'}**`,
362
+ `> Request: *"${meta.userRequest || ''}"*`,
363
+ `> Date: ${meta.timestamp || new Date().toISOString()}`,
364
+ `> Documents: ${successful.length}`,
365
+ '',
366
+ '---',
367
+ '',
368
+ '## Document Index',
369
+ '',
370
+ ];
371
+
372
+ // Group by category
373
+ const categories = {};
374
+ for (const doc of successful) {
375
+ const cat = doc.topic.category || 'other';
376
+ if (!categories[cat]) categories[cat] = [];
377
+ categories[cat].push(doc);
378
+ }
379
+
380
+ const categoryLabels = {
381
+ 'overview': 'Overview',
382
+ 'guide': 'Guides & How-To',
383
+ 'analysis': 'Analysis & Evaluation',
384
+ 'plan': 'Plans & Strategy',
385
+ 'reference': 'Reference Material',
386
+ 'concept': 'Concepts & Theory',
387
+ 'decision': 'Decisions & Trade-offs',
388
+ 'checklist': 'Checklists & Verification',
389
+ 'template': 'Templates & Patterns',
390
+ 'report': 'Reports & Findings',
391
+ };
392
+
393
+ for (const [cat, docs] of Object.entries(categories)) {
394
+ indexLines.push(`### ${categoryLabels[cat] || cat}`);
395
+ indexLines.push('');
396
+ for (const doc of docs) {
397
+ const audience = doc.topic.target_audience ? ` *(${doc.topic.target_audience})*` : '';
398
+ indexLines.push(`- **[${doc.topic.title}](${doc._fileName})**${audience}`);
399
+ indexLines.push(` ${doc.topic.description}`);
400
+ }
401
+ indexLines.push('');
402
+ }
403
+
404
+ // Stats
405
+ const totalTokens = documents.reduce((s, d) => s + (d.tokenUsage?.totalTokens || 0), 0);
406
+ const totalDuration = documents.reduce((s, d) => s + (d.durationMs || 0), 0);
407
+
408
+ indexLines.push('---');
409
+ indexLines.push('');
410
+ indexLines.push(`*${successful.length} documents generated | ${totalTokens.toLocaleString()} tokens | ${(totalDuration / 1000).toFixed(1)}s*`);
411
+
412
+ if (failed.length > 0) {
413
+ indexLines.push('');
414
+ indexLines.push(`> ⚠ ${failed.length} document(s) failed to generate:`);
415
+ for (const doc of failed) {
416
+ indexLines.push(`> - ${doc.topic.title}: ${doc.error}`);
417
+ }
418
+ }
419
+
420
+ const indexPath = path.join(outputDir, 'INDEX.md');
421
+ fs.writeFileSync(indexPath, indexLines.join('\n'), 'utf8');
422
+ docPaths.unshift(indexPath);
423
+
424
+ // Write metadata JSON
425
+ const metaPath = path.join(outputDir, 'dynamic-run.json');
426
+ fs.writeFileSync(metaPath, JSON.stringify({
427
+ timestamp: meta.timestamp,
428
+ folderName: meta.folderName,
429
+ userRequest: meta.userRequest,
430
+ projectSummary: meta.projectSummary,
431
+ topicCount: successful.length,
432
+ failedCount: failed.length,
433
+ totalTokens,
434
+ totalDurationMs: totalDuration,
435
+ topics: documents.map(d => ({
436
+ id: d.topic.id,
437
+ title: d.topic.title,
438
+ category: d.topic.category,
439
+ fileName: d._fileName || null,
440
+ success: !!d.markdown,
441
+ error: d.error || null,
442
+ tokens: d.tokenUsage?.totalTokens || 0,
443
+ durationMs: d.durationMs,
444
+ })),
445
+ }, null, 2), 'utf8');
446
+ docPaths.push(metaPath);
447
+
448
+ return {
449
+ indexPath,
450
+ docPaths,
451
+ stats: {
452
+ total: documents.length,
453
+ successful: successful.length,
454
+ failed: failed.length,
455
+ totalTokens,
456
+ totalDurationMs: totalDuration,
457
+ },
458
+ };
459
+ }
460
+
461
+ // ======================== HELPERS ========================
462
+
463
+ /**
464
+ * Get category-specific writing guidance for dynamic mode.
465
+ */
466
+ function getDynamicCategoryGuidance(category) {
467
+ const guides = {
468
+ 'overview': `CATEGORY GUIDANCE — OVERVIEW:
469
+ Write a high-level overview that serves as an introduction and navigation aid.
470
+ - Summarize the entire scope of the document set
471
+ - Explain the "why" — why this document set exists
472
+ - Provide a reading order recommendation
473
+ - Keep it concise but comprehensive`,
474
+
475
+ 'guide': `CATEGORY GUIDANCE — GUIDE:
476
+ Write a practical, hands-on guide with clear steps.
477
+ - Use numbered steps for sequential processes
478
+ - Include prerequisites at the top
479
+ - Add code examples, commands, or configuration snippets
480
+ - Include "common pitfalls" or "troubleshooting" sections
481
+ - Make steps testable/verifiable`,
482
+
483
+ 'analysis': `CATEGORY GUIDANCE — ANALYSIS:
484
+ Write an analytical document with evidence-based reasoning.
485
+ - Use comparison tables for alternatives
486
+ - Include pros/cons or SWOT where relevant
487
+ - Support claims with data from context docs
488
+ - Include risk assessments where appropriate
489
+ - End with clear conclusions or recommendations`,
490
+
491
+ 'plan': `CATEGORY GUIDANCE — PLAN:
492
+ Write an actionable plan with clear milestones.
493
+ - Include timeline or phases
494
+ - Define owners/responsibilities where possible
495
+ - List dependencies between steps
496
+ - Include risk mitigation strategies
497
+ - Add success criteria or KPIs`,
498
+
499
+ 'reference': `CATEGORY GUIDANCE — REFERENCE:
500
+ Write clear, well-structured reference material.
501
+ - Use tables extensively for structured data
502
+ - Include examples for each concept
503
+ - Organize alphabetically or by logical grouping
504
+ - Make it scannable with clear headings
505
+ - Include cross-references to related docs`,
506
+
507
+ 'concept': `CATEGORY GUIDANCE — CONCEPT EXPLANATION:
508
+ Write a clear educational explanation.
509
+ - Start with "what it is" for newcomers
510
+ - Explain "why it matters" in context
511
+ - Use analogies to make complex ideas accessible
512
+ - Include diagrams (as described text) if helpful
513
+ - Progress from simple to advanced`,
514
+
515
+ 'decision': `CATEGORY GUIDANCE — DECISION RECORD:
516
+ Write an Architecture/Engineering Decision Record.
517
+ - "Context" — what situation requires a decision
518
+ - "Options" — what alternatives exist (with pros/cons)
519
+ - "Decision" — what was chosen and why
520
+ - "Consequences" — what this means going forward
521
+ - "Review Date" — when to reassess (if applicable)`,
522
+
523
+ 'checklist': `CATEGORY GUIDANCE — CHECKLIST:
524
+ Write an actionable checklist with clear verification criteria.
525
+ - Use checkbox syntax (- [ ]) for items
526
+ - Group items by phase or category
527
+ - Include "done when" criteria for each item
528
+ - Add notes for non-obvious items
529
+ - Keep items concise and actionable`,
530
+
531
+ 'template': `CATEGORY GUIDANCE — TEMPLATE:
532
+ Create a reusable template with clear structure.
533
+ - Include placeholder text showing expected content
534
+ - Add instructions/comments explaining each section
535
+ - Make it copy-paste ready
536
+ - Include examples of filled-out sections
537
+ - Keep it flexible but structured`,
538
+
539
+ 'report': `CATEGORY GUIDANCE — REPORT:
540
+ Write a clear findings/status report.
541
+ - Start with executive summary
542
+ - Use data and metrics where available
543
+ - Include visualizations as text tables
544
+ - Separate observations from recommendations
545
+ - End with clear next steps`,
546
+ };
547
+
548
+ return guides[category] || 'Write a clear, well-structured, professional document.';
549
+ }
550
+
551
+ /**
552
+ * Convert title to URL-safe slug.
553
+ */
554
+ function slugify(text) {
555
+ return text
556
+ .toLowerCase()
557
+ .replace(/[^a-z0-9]+/g, '-')
558
+ .replace(/^-+|-+$/g, '')
559
+ .slice(0, 60);
560
+ }
561
+
562
+ module.exports = {
563
+ planTopics,
564
+ generateDynamicDocument,
565
+ generateAllDynamicDocuments,
566
+ writeDynamicOutput,
567
+ };