@iservu-inc/adf-cli 0.10.0 → 0.12.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 (40) hide show
  1. package/.adf/feature-audit.md +208 -0
  2. package/.adf/final-summary.md +347 -0
  3. package/.adf/implementation-plan.md +244 -0
  4. package/.adf/implementation-progress.md +203 -0
  5. package/.adf/learning/answer-history.json +995 -0
  6. package/.adf/learning/config.json +25 -0
  7. package/.adf/learning/learned-rules.json +59 -0
  8. package/.adf/learning/patterns.json +277 -0
  9. package/.adf/learning/skip-history.json +1451 -0
  10. package/.adf/learning/stats.json +9 -0
  11. package/.claude/settings.local.json +10 -1
  12. package/.project/chats/current/SESSION-STATUS.md +102 -73
  13. package/.project/docs/ROADMAP.md +47 -32
  14. package/.project/docs/designs/LEARNING-ANALYTICS-DASHBOARD.md +1383 -0
  15. package/CHANGELOG.md +341 -0
  16. package/CLAUDE.md +479 -0
  17. package/README.md +7 -1
  18. package/lib/commands/deploy.js +42 -2
  19. package/lib/generators/agents-md-generator.js +431 -161
  20. package/lib/generators/antigravity-generator.js +140 -0
  21. package/lib/generators/index.js +22 -0
  22. package/lib/generators/zed-generator.js +252 -0
  23. package/lib/learning/analytics-exporter.js +241 -0
  24. package/lib/learning/analytics-view.js +508 -0
  25. package/lib/learning/analytics.js +681 -0
  26. package/lib/learning/learning-manager.js +19 -6
  27. package/lib/templates/shared/agents/architect.md +24 -24
  28. package/lib/templates/shared/agents/dev.md +25 -20
  29. package/lib/templates/shared/agents/pm.md +14 -4
  30. package/lib/templates/shared/agents/sm.md +18 -14
  31. package/lib/templates/shared/templates/openspec-delta.md +16 -0
  32. package/lib/templates/shared/templates/openspec-proposal.md +18 -0
  33. package/lib/templates/shared/templates/openspec-tasks.md +21 -0
  34. package/lib/utils/context-manager.js +484 -0
  35. package/package.json +6 -1
  36. package/scripts/generate-test-data.js +557 -0
  37. package/tests/agents-md-generator.test.js +47 -10
  38. package/tests/analytics-exporter.test.js +477 -0
  39. package/tests/analytics-view.test.js +466 -0
  40. package/tests/analytics.test.js +712 -0
@@ -0,0 +1,140 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const ToolConfigGenerator = require('./tool-config-generator');
4
+
5
+ /**
6
+ * Generator for Google Antigravity configurations
7
+ * Creates .antigravity/agents.yaml that mounts AGENTS.md
8
+ * Provides file system access to .context/ directory
9
+ */
10
+ class AntigravityGenerator extends ToolConfigGenerator {
11
+ /**
12
+ * Generate Antigravity configurations
13
+ * @returns {Object} Generated file paths
14
+ */
15
+ async generate() {
16
+ await this.initialize();
17
+
18
+ const antigravityDir = path.join(this.projectPath, '.antigravity');
19
+ await fs.ensureDir(antigravityDir);
20
+
21
+ const generated = {
22
+ agents: null
23
+ };
24
+
25
+ // Generate agents.yaml
26
+ generated.agents = await this.generateAgentsYaml(antigravityDir);
27
+
28
+ return generated;
29
+ }
30
+
31
+ /**
32
+ * Generate .antigravity/agents.yaml
33
+ */
34
+ async generateAgentsYaml(antigravityDir) {
35
+ const agentsPath = path.join(antigravityDir, 'agents.yaml');
36
+
37
+ const agentName = this.getAgentName();
38
+ const model = this.getModelForFramework();
39
+
40
+ // Generate YAML content
41
+ const yamlContent = `# Antigravity Agent Configuration
42
+ # Generated by ADF CLI v${this.getADFVersion()}
43
+ # Framework: ${this.getFrameworkName()}
44
+
45
+ agents:
46
+ - name: "${agentName}"
47
+ model: "${model}"
48
+ capabilities:
49
+ - file_system:
50
+ read_only:
51
+ - "AGENTS.md"
52
+ - ".context"
53
+ - ".adf/sessions"
54
+ exclude:
55
+ - ".adf/.env"
56
+ - "node_modules"
57
+ - ".git"
58
+ system_prompt: |
59
+ You are ${agentName}, an expert AI coding assistant.
60
+
61
+ CRITICAL INSTRUCTIONS:
62
+ 1. Read "AGENTS.md" IMMEDIATELY upon startup to understand project rules and structure
63
+ 2. Consult ".context/memory/architecture.md" for system design decisions
64
+ 3. Follow all operational rules defined in AGENTS.md (they are NON-NEGOTIABLE)
65
+ 4. Never output secrets from .env files or expose API keys
66
+ 5. All code changes must pass the test suite defined in AGENTS.md
67
+
68
+ WORKFLOW:
69
+ - Before any code change, read AGENTS.md and .context/memory/architecture.md
70
+ - Follow the build & test commands specified in AGENTS.md
71
+ - Adhere to the project structure and workflow directives
72
+ - Ask clarifying questions if requirements are unclear
73
+
74
+ PROJECT CONTEXT:
75
+ - Framework: ${this.getFrameworkName()}
76
+ - Session: ${this.getSessionId()}
77
+ - Complete requirements: .adf/sessions/${this.getSessionId()}/outputs/
78
+
79
+ Remember: AGENTS.md is your single source of truth. Read it first, follow it always.
80
+ `;
81
+
82
+ await fs.writeFile(agentsPath, yamlContent, 'utf-8');
83
+ return agentsPath;
84
+ }
85
+
86
+ /**
87
+ * Get agent name based on framework
88
+ */
89
+ getAgentName() {
90
+ const projectName = this.getProjectName();
91
+
92
+ const roleMap = {
93
+ 'rapid': 'Developer',
94
+ 'balanced': 'Project Architect',
95
+ 'comprehensive': 'Solutions Architect'
96
+ };
97
+
98
+ const role = roleMap[this.framework] || 'Developer';
99
+ return `${projectName} ${role}`;
100
+ }
101
+
102
+ /**
103
+ * Get appropriate model for framework complexity
104
+ */
105
+ getModelForFramework() {
106
+ const modelMap = {
107
+ 'rapid': 'gemini-2.0-flash-exp',
108
+ 'balanced': 'gemini-2.0-flash-thinking-exp',
109
+ 'comprehensive': 'gemini-exp-1206'
110
+ };
111
+
112
+ return modelMap[this.framework] || 'gemini-2.0-flash-exp';
113
+ }
114
+
115
+ /**
116
+ * Get framework display name
117
+ */
118
+ getFrameworkName() {
119
+ const names = {
120
+ 'rapid': 'Rapid Development (PRP)',
121
+ 'balanced': 'Balanced (Specification-Driven)',
122
+ 'comprehensive': 'BMAD Comprehensive (Enterprise)'
123
+ };
124
+ return names[this.framework] || this.framework;
125
+ }
126
+
127
+ /**
128
+ * Get ADF CLI version
129
+ */
130
+ getADFVersion() {
131
+ try {
132
+ const packageJson = require('../../package.json');
133
+ return packageJson.version;
134
+ } catch (error) {
135
+ return '0.11.0';
136
+ }
137
+ }
138
+ }
139
+
140
+ module.exports = AntigravityGenerator;
@@ -7,6 +7,8 @@ const AgentsMdGenerator = require('./agents-md-generator');
7
7
  const WindsurfGenerator = require('./windsurf-generator');
8
8
  const CursorGenerator = require('./cursor-generator');
9
9
  const VSCodeGenerator = require('./vscode-generator');
10
+ const ZedGenerator = require('./zed-generator');
11
+ const AntigravityGenerator = require('./antigravity-generator');
10
12
  const ToolConfigGenerator = require('./tool-config-generator');
11
13
 
12
14
  /**
@@ -84,15 +86,35 @@ async function generateVSCode(sessionPath, projectPath, framework) {
84
86
  return await generator.generate();
85
87
  }
86
88
 
89
+ /**
90
+ * Generate Zed Editor configurations
91
+ */
92
+ async function generateZed(sessionPath, projectPath, framework) {
93
+ const generator = new ZedGenerator(sessionPath, projectPath, framework);
94
+ return await generator.generate();
95
+ }
96
+
97
+ /**
98
+ * Generate Google Antigravity configurations
99
+ */
100
+ async function generateAntigravity(sessionPath, projectPath, framework) {
101
+ const generator = new AntigravityGenerator(sessionPath, projectPath, framework);
102
+ return await generator.generate();
103
+ }
104
+
87
105
  module.exports = {
88
106
  generateAll,
89
107
  generateAgentsMd,
90
108
  generateWindsurf,
91
109
  generateCursor,
92
110
  generateVSCode,
111
+ generateZed,
112
+ generateAntigravity,
93
113
  AgentsMdGenerator,
94
114
  WindsurfGenerator,
95
115
  CursorGenerator,
96
116
  VSCodeGenerator,
117
+ ZedGenerator,
118
+ AntigravityGenerator,
97
119
  ToolConfigGenerator
98
120
  };
@@ -0,0 +1,252 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const ToolConfigGenerator = require('./tool-config-generator');
4
+
5
+ /**
6
+ * Generator for Zed Editor configurations
7
+ * Creates .zed/settings.json with MCP integration and agent configuration
8
+ * Creates .zed/keymap.json for agent switching shortcuts
9
+ * Creates symlink .zed/rules -> ../AGENTS.md (or copies on Windows)
10
+ */
11
+ class ZedGenerator extends ToolConfigGenerator {
12
+ /**
13
+ * Generate Zed Editor configurations
14
+ * @returns {Object} Generated file paths
15
+ */
16
+ async generate() {
17
+ await this.initialize();
18
+
19
+ const zedDir = path.join(this.projectPath, '.zed');
20
+ await fs.ensureDir(zedDir);
21
+
22
+ const generated = {
23
+ settings: null,
24
+ keymap: null,
25
+ rules: null
26
+ };
27
+
28
+ // Generate settings.json
29
+ generated.settings = await this.generateSettings(zedDir);
30
+
31
+ // Generate keymap.json
32
+ generated.keymap = await this.generateKeymap(zedDir);
33
+
34
+ // Create symlink or copy AGENTS.md
35
+ generated.rules = await this.linkAgentsMd(zedDir);
36
+
37
+ return generated;
38
+ }
39
+
40
+ /**
41
+ * Generate .zed/settings.json with MCP and agent configuration
42
+ */
43
+ async generateSettings(zedDir) {
44
+ const settingsPath = path.join(zedDir, 'settings.json');
45
+
46
+ // Determine model configuration based on framework
47
+ const modelConfig = this.getModelConfiguration();
48
+
49
+ const settings = {
50
+ agent: {
51
+ default_model: modelConfig.default,
52
+ feature_specific_models: modelConfig.featureSpecific,
53
+ model_parameters: modelConfig.parameters,
54
+ always_allow_tool_actions: true,
55
+ single_file_review: true
56
+ },
57
+ context_servers: this.getMCPContextServers(),
58
+ terminal: {
59
+ env: this.getTerminalEnvironment()
60
+ }
61
+ };
62
+
63
+ await fs.writeJson(settingsPath, settings, { spaces: 2 });
64
+ return settingsPath;
65
+ }
66
+
67
+ /**
68
+ * Get model configuration based on framework complexity
69
+ */
70
+ getModelConfiguration() {
71
+ const configs = {
72
+ rapid: {
73
+ default: {
74
+ provider: 'anthropic',
75
+ model: 'claude-3-5-sonnet-20241022'
76
+ },
77
+ featureSpecific: {
78
+ thread_summary_model: {
79
+ provider: 'google',
80
+ model: 'gemini-2.0-flash-exp'
81
+ },
82
+ commit_message_model: {
83
+ provider: 'google',
84
+ model: 'gemini-2.0-flash-exp'
85
+ },
86
+ inline_assistant_model: {
87
+ provider: 'anthropic',
88
+ model: 'claude-3-5-sonnet-20241022'
89
+ }
90
+ },
91
+ parameters: [
92
+ { provider: 'anthropic', temperature: 0.1 },
93
+ { provider: 'google', temperature: 0.2 }
94
+ ]
95
+ },
96
+ balanced: {
97
+ default: {
98
+ provider: 'anthropic',
99
+ model: 'claude-3-5-sonnet-20241022'
100
+ },
101
+ featureSpecific: {
102
+ thread_summary_model: {
103
+ provider: 'google',
104
+ model: 'gemini-2.0-flash-exp'
105
+ },
106
+ commit_message_model: {
107
+ provider: 'google',
108
+ model: 'gemini-2.0-flash-exp'
109
+ },
110
+ inline_assistant_model: {
111
+ provider: 'anthropic',
112
+ model: 'claude-3-5-sonnet-20241022'
113
+ }
114
+ },
115
+ parameters: [
116
+ { provider: 'anthropic', temperature: 0.1 },
117
+ { provider: 'google', temperature: 0.2 }
118
+ ]
119
+ },
120
+ comprehensive: {
121
+ default: {
122
+ provider: 'anthropic',
123
+ model: 'claude-3-5-sonnet-20241022'
124
+ },
125
+ featureSpecific: {
126
+ thread_summary_model: {
127
+ provider: 'anthropic',
128
+ model: 'claude-3-5-sonnet-20241022'
129
+ },
130
+ commit_message_model: {
131
+ provider: 'anthropic',
132
+ model: 'claude-3-5-sonnet-20241022'
133
+ },
134
+ inline_assistant_model: {
135
+ provider: 'anthropic',
136
+ model: 'claude-3-5-sonnet-20241022'
137
+ }
138
+ },
139
+ parameters: [
140
+ { provider: 'anthropic', temperature: 0.05 }
141
+ ]
142
+ }
143
+ };
144
+
145
+ return configs[this.framework] || configs.balanced;
146
+ }
147
+
148
+ /**
149
+ * Get MCP context servers configuration
150
+ */
151
+ getMCPContextServers() {
152
+ const servers = {
153
+ project_context: {
154
+ command: 'npx',
155
+ args: ['@modelcontextprotocol/server-filesystem', '.context']
156
+ }
157
+ };
158
+
159
+ // Add Archon if configured
160
+ const archonUrl = process.env.ARCHON_MCP_URL || this.metadata?.archonUrl;
161
+ if (archonUrl) {
162
+ servers.archon = {
163
+ url: archonUrl
164
+ };
165
+ }
166
+
167
+ return servers;
168
+ }
169
+
170
+ /**
171
+ * Get terminal environment variables
172
+ */
173
+ getTerminalEnvironment() {
174
+ const env = {};
175
+
176
+ // Add ARCHON_MCP_PORT if configured
177
+ if (process.env.ARCHON_MCP_URL) {
178
+ const url = new URL(process.env.ARCHON_MCP_URL);
179
+ if (url.port) {
180
+ env.ARCHON_MCP_PORT = url.port;
181
+ }
182
+ }
183
+
184
+ return env;
185
+ }
186
+
187
+ /**
188
+ * Generate .zed/keymap.json with agent switching shortcuts
189
+ */
190
+ async generateKeymap(zedDir) {
191
+ const keymapPath = path.join(zedDir, 'keymap.json');
192
+
193
+ const keymap = [
194
+ {
195
+ context: 'Editor',
196
+ bindings: {
197
+ 'ctrl-shift-a': 'assistant::Toggle',
198
+ 'ctrl-shift-c': 'assistant::CycleMessageRole',
199
+ 'ctrl-shift-r': 'assistant::Restart'
200
+ }
201
+ },
202
+ {
203
+ context: 'AssistantPanel',
204
+ bindings: {
205
+ 'ctrl-enter': 'assistant::Assist',
206
+ 'escape': 'assistant::Hide',
207
+ 'ctrl-shift-n': 'assistant::NewConversation'
208
+ }
209
+ }
210
+ ];
211
+
212
+ await fs.writeJson(keymapPath, keymap, { spaces: 2 });
213
+ return keymapPath;
214
+ }
215
+
216
+ /**
217
+ * Create symlink .zed/rules -> ../AGENTS.md
218
+ * Falls back to file copy on Windows if symlink fails
219
+ */
220
+ async linkAgentsMd(zedDir) {
221
+ const rulesPath = path.join(zedDir, 'rules');
222
+ const agentsMdPath = path.join(this.projectPath, 'AGENTS.md');
223
+
224
+ // Remove existing link/file if present
225
+ if (await fs.pathExists(rulesPath)) {
226
+ await fs.remove(rulesPath);
227
+ }
228
+
229
+ // Check if AGENTS.md exists
230
+ if (!await fs.pathExists(agentsMdPath)) {
231
+ console.warn('AGENTS.md not found, skipping Zed rules link');
232
+ return null;
233
+ }
234
+
235
+ try {
236
+ // Try to create symlink (Unix/Mac/modern Windows)
237
+ await fs.symlink('../AGENTS.md', rulesPath, 'file');
238
+ return { type: 'symlink', path: rulesPath };
239
+ } catch (error) {
240
+ // Symlink failed (older Windows or permissions), copy file instead
241
+ try {
242
+ await fs.copy(agentsMdPath, rulesPath);
243
+ return { type: 'copy', path: rulesPath };
244
+ } catch (copyError) {
245
+ console.warn(`Failed to create Zed rules link: ${copyError.message}`);
246
+ return null;
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ module.exports = ZedGenerator;
@@ -0,0 +1,241 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Learning Analytics Exporter
6
+ *
7
+ * Handles exporting analytics data in multiple formats:
8
+ * - JSON: Single comprehensive file with all analytics
9
+ * - CSV: Multiple files for different metrics
10
+ */
11
+
12
+ /**
13
+ * Export analytics data
14
+ * @param {Object} analyticsData - Complete analytics data
15
+ * @param {string} projectPath - Project root path
16
+ * @param {string} format - Export format ('json' or 'csv')
17
+ * @returns {Promise<Object>} Export result with file paths
18
+ */
19
+ async function exportAnalytics(analyticsData, projectPath, format = 'json') {
20
+ const learningPath = path.join(projectPath, '.adf', 'learning');
21
+ const exportsPath = path.join(learningPath, 'exports');
22
+
23
+ // Ensure exports directory exists
24
+ await fs.ensureDir(exportsPath);
25
+
26
+ if (format === 'json') {
27
+ return await exportJSON(analyticsData, exportsPath);
28
+ } else if (format === 'csv') {
29
+ return await exportCSV(analyticsData, exportsPath);
30
+ } else {
31
+ throw new Error(`Unsupported export format: ${format}`);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Export analytics as JSON
37
+ * @param {Object} analyticsData - Analytics data
38
+ * @param {string} exportsPath - Exports directory path
39
+ * @returns {Promise<Object>} Export result
40
+ */
41
+ async function exportJSON(analyticsData, exportsPath) {
42
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
43
+ const filename = `analytics-${timestamp}.json`;
44
+ const filePath = path.join(exportsPath, filename);
45
+
46
+ // Write JSON file
47
+ await fs.writeJSON(filePath, analyticsData, { spaces: 2 });
48
+
49
+ // Get file size
50
+ const stats = await fs.stat(filePath);
51
+ const sizeKB = (stats.size / 1024).toFixed(2);
52
+
53
+ return {
54
+ file: filePath,
55
+ size: `${sizeKB} KB`,
56
+ format: 'json'
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Export analytics as CSV (multiple files)
62
+ * @param {Object} analyticsData - Analytics data
63
+ * @param {string} exportsPath - Exports directory path
64
+ * @returns {Promise<Object>} Export result
65
+ */
66
+ async function exportCSV(analyticsData, exportsPath) {
67
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
68
+ const files = [];
69
+
70
+ // Export overview
71
+ const overviewFile = path.join(exportsPath, `analytics-overview-${timestamp}.csv`);
72
+ await fs.writeFile(overviewFile, generateOverviewCSV(analyticsData.overview));
73
+ files.push(overviewFile);
74
+
75
+ // Export skip trends
76
+ const trendsFile = path.join(exportsPath, `analytics-skip-trends-${timestamp}.csv`);
77
+ await fs.writeFile(trendsFile, generateSkipTrendsCSV(analyticsData.skipTrends));
78
+ files.push(trendsFile);
79
+
80
+ // Export category preferences
81
+ const categoriesFile = path.join(exportsPath, `analytics-categories-${timestamp}.csv`);
82
+ await fs.writeFile(categoriesFile, generateCategoriesCSV(analyticsData.categoryPreferences));
83
+ files.push(categoriesFile);
84
+
85
+ // Export patterns
86
+ const patternsFile = path.join(exportsPath, `analytics-patterns-${timestamp}.csv`);
87
+ await fs.writeFile(patternsFile, generatePatternsCSV(analyticsData.patternDistribution, analyticsData.decayStatus));
88
+ files.push(patternsFile);
89
+
90
+ // Export effectiveness
91
+ const effectivenessFile = path.join(exportsPath, `analytics-effectiveness-${timestamp}.csv`);
92
+ await fs.writeFile(effectivenessFile, generateEffectivenessCSV(analyticsData.effectiveness));
93
+ files.push(effectivenessFile);
94
+
95
+ return {
96
+ files,
97
+ format: 'csv'
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Generate overview CSV
103
+ * @param {Object} overview - Overview data
104
+ * @returns {string} CSV content
105
+ */
106
+ function generateOverviewCSV(overview) {
107
+ const rows = [
108
+ ['Metric', 'Value', 'Unit'],
109
+ ['Total Sessions', overview.totalSessions, 'sessions'],
110
+ ['Total Skips', overview.totalSkips, 'questions'],
111
+ ['Manual Skips', overview.manualSkips, 'questions'],
112
+ ['Filtered Skips', overview.filteredSkips, 'questions'],
113
+ ['Total Answers', overview.totalAnswers, 'questions'],
114
+ ['Active Patterns', overview.activePatterns, 'patterns'],
115
+ ['Active Rules', overview.activeRules, 'rules'],
116
+ ['Learning Age', overview.learningAge, 'days']
117
+ ];
118
+
119
+ return rows.map(row => row.join(',')).join('\n');
120
+ }
121
+
122
+ /**
123
+ * Generate skip trends CSV
124
+ * @param {Array} skipTrends - Skip trends data
125
+ * @returns {string} CSV content
126
+ */
127
+ function generateSkipTrendsCSV(skipTrends) {
128
+ const rows = [
129
+ ['Week', 'Week Number', 'Manual Skips', 'Filtered Skips', 'Total Skips', 'Skip Rate (%)']
130
+ ];
131
+
132
+ for (const trend of skipTrends) {
133
+ rows.push([
134
+ trend.week,
135
+ trend.weekNumber,
136
+ trend.manualSkips,
137
+ trend.filteredSkips,
138
+ trend.totalSkips,
139
+ trend.skipRate
140
+ ]);
141
+ }
142
+
143
+ return rows.map(row => row.join(',')).join('\n');
144
+ }
145
+
146
+ /**
147
+ * Generate categories CSV
148
+ * @param {Array} categories - Category preferences data
149
+ * @returns {string} CSV content
150
+ */
151
+ function generateCategoriesCSV(categories) {
152
+ const rows = [
153
+ ['Category', 'Skips', 'Answers', 'Total', 'Skip Rate (%)', 'Level']
154
+ ];
155
+
156
+ for (const cat of categories) {
157
+ rows.push([
158
+ escapeCSV(cat.category),
159
+ cat.skips,
160
+ cat.answers,
161
+ cat.total,
162
+ cat.skipRate,
163
+ cat.level
164
+ ]);
165
+ }
166
+
167
+ return rows.map(row => row.join(',')).join('\n');
168
+ }
169
+
170
+ /**
171
+ * Generate patterns CSV
172
+ * @param {Object} patternDistribution - Pattern distribution data
173
+ * @param {Object} decayStatus - Decay status data
174
+ * @returns {string} CSV content
175
+ */
176
+ function generatePatternsCSV(patternDistribution, decayStatus) {
177
+ const dist = patternDistribution.distribution || { high: 0, medium: 0, low: 0, veryLow: 0 };
178
+ const decay = decayStatus || { healthy: 0, warning: 0, critical: 0, recentlyRenewed: 0, avgAge: 0 };
179
+
180
+ const rows = [
181
+ ['Metric', 'Value', 'Description'],
182
+ ['Total Patterns', patternDistribution.totalPatterns || 0, 'All patterns'],
183
+ ['High Confidence (90%+)', dist.high, 'Very strong patterns'],
184
+ ['Medium Confidence (75-89%)', dist.medium, 'Strong patterns'],
185
+ ['Low Confidence (50-74%)', dist.low, 'Weak patterns'],
186
+ ['Very Low Confidence (<50%)', dist.veryLow, 'Very weak patterns'],
187
+ ['Average Confidence', (patternDistribution.avgConfidence || 0) + '%', 'Mean confidence score'],
188
+ ['At Risk', patternDistribution.atRiskCount || 0, 'Patterns at risk of removal'],
189
+ ['', '', ''],
190
+ ['Decay Status', '', ''],
191
+ ['Healthy Patterns', decay.healthy, 'Active and strong'],
192
+ ['Warning Patterns', decay.warning, 'Needs attention'],
193
+ ['Critical Patterns', decay.critical, 'May be removed soon'],
194
+ ['Recently Renewed', decay.recentlyRenewed, 'Last 30 days'],
195
+ ['Average Age', decay.avgAge + ' days', 'Mean pattern age']
196
+ ];
197
+
198
+ return rows.map(row => row.join(',')).join('\n');
199
+ }
200
+
201
+ /**
202
+ * Generate effectiveness CSV
203
+ * @param {Object} effectiveness - Effectiveness data
204
+ * @returns {string} CSV content
205
+ */
206
+ function generateEffectivenessCSV(effectiveness) {
207
+ const rows = [
208
+ ['Metric', 'Value', 'Unit'],
209
+ ['Time Saved', effectiveness.timeSavedMinutes, 'minutes'],
210
+ ['Time Saved (Hours)', effectiveness.timeSavedHours, 'hours'],
211
+ ['Questions Filtered', effectiveness.questionsFiltered, 'questions'],
212
+ ['False Positives', effectiveness.falsePositives, 'questions'],
213
+ ['False Positive Rate', effectiveness.falsePositiveRate, '%'],
214
+ ['Pattern Accuracy', effectiveness.patternAccuracy, '%'],
215
+ ['Total Rule Applications', effectiveness.totalRuleApplications, 'applications'],
216
+ ['Avg Applications Per Rule', effectiveness.avgApplicationsPerRule, 'applications'],
217
+ ['Overall Effectiveness', effectiveness.overallEffectiveness, '%']
218
+ ];
219
+
220
+ return rows.map(row => row.join(',')).join('\n');
221
+ }
222
+
223
+ /**
224
+ * Escape CSV value
225
+ * @param {string} value - Value to escape
226
+ * @returns {string} Escaped value
227
+ */
228
+ function escapeCSV(value) {
229
+ if (typeof value !== 'string') return value;
230
+
231
+ // Escape quotes and wrap in quotes if contains special chars
232
+ if (value.includes(',') || value.includes('"') || value.includes('\n')) {
233
+ return '"' + value.replace(/"/g, '""') + '"';
234
+ }
235
+
236
+ return value;
237
+ }
238
+
239
+ module.exports = {
240
+ exportAnalytics
241
+ };