@polymorphism-tech/morph-spec 4.2.0 → 4.3.1

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 (140) hide show
  1. package/CLAUDE.md +108 -946
  2. package/bin/morph-spec.js +284 -9
  3. package/bin/task-manager.cjs +102 -14
  4. package/bin/validate.js +4 -4
  5. package/docs/{v3.0 → next-generation}/AGENTS.md +1 -1
  6. package/docs/next-generation/CONTEXT-OPTIMIZATION.md +267 -0
  7. package/docs/next-generation/EXECUTION-FLOW.md +274 -0
  8. package/docs/next-generation/META-PROMPTS.md +235 -0
  9. package/docs/next-generation/MIGRATION-GUIDE.md +253 -0
  10. package/docs/next-generation/THREAD-MANAGEMENT.md +240 -0
  11. package/package.json +5 -5
  12. package/src/commands/agents/agents-fuse.js +97 -0
  13. package/src/commands/agents/micro-agent.js +112 -0
  14. package/src/commands/agents/spawn-team.js +69 -4
  15. package/src/commands/agents/squad-template.js +146 -0
  16. package/src/commands/analytics/analytics.js +176 -0
  17. package/src/commands/context/context-prime.js +63 -0
  18. package/src/commands/context/core-four.js +54 -0
  19. package/src/commands/mcp/mcp.js +102 -0
  20. package/src/commands/project/detect-agents.js +32 -2
  21. package/src/commands/project/detect.js +11 -1
  22. package/src/commands/project/doctor.js +573 -356
  23. package/src/commands/project/init.js +9 -2
  24. package/src/commands/project/update.js +13 -3
  25. package/src/commands/state/advance-phase.js +448 -416
  26. package/src/commands/state/state.js +14 -12
  27. package/src/commands/tasks/task.js +1 -1
  28. package/src/commands/templates/template-render.js +80 -1
  29. package/src/commands/threads/thread-template.js +103 -0
  30. package/src/commands/threads/threads.js +261 -0
  31. package/src/commands/trust/trust.js +205 -0
  32. package/src/{orchestrator.js → core/orchestrator.js} +8 -8
  33. package/src/core/state/state-manager.js +37 -17
  34. package/src/core/workflows/workflow-detector.js +114 -3
  35. package/src/lib/agents/micro-agent-factory.js +161 -0
  36. package/src/lib/analytics/analytics-engine.js +345 -0
  37. package/src/lib/checkpoints/checkpoint-hooks.js +298 -258
  38. package/src/lib/context/context-bundler.js +240 -0
  39. package/src/lib/context/context-optimizer.js +212 -0
  40. package/src/lib/context/context-tracker.js +273 -0
  41. package/src/lib/context/core-four-tracker.js +201 -0
  42. package/src/lib/context/mcp-optimizer.js +200 -0
  43. package/src/lib/detectors/index.js +1 -1
  44. package/src/lib/detectors/standards-generator.js +77 -17
  45. package/src/lib/detectors/structure-detector.js +67 -39
  46. package/src/lib/execution/fusion-executor.js +304 -0
  47. package/src/lib/execution/parallel-executor.js +270 -0
  48. package/src/lib/generators/context-generator.js +3 -3
  49. package/src/lib/generators/recap-generator.js +32 -12
  50. package/src/lib/hooks/hook-executor.js +169 -0
  51. package/src/lib/hooks/stop-hook-executor.js +286 -0
  52. package/src/lib/hops/hop-composer.js +221 -0
  53. package/src/lib/threads/thread-coordinator.js +238 -0
  54. package/src/lib/threads/thread-manager.js +317 -0
  55. package/src/lib/tracking/artifact-trail.js +202 -0
  56. package/src/lib/trust/trust-manager.js +269 -0
  57. package/src/lib/validators/design-system/design-system-validator.js +2 -2
  58. package/src/lib/validators/validation-runner.js +14 -30
  59. package/src/utils/hooks-installer.js +69 -0
  60. package/stacks/blazor-azure/.morph/config/agents.json +72 -3
  61. package/stacks/nextjs-supabase/.morph/config/agents.json +3 -3
  62. package/docs/llm-interaction-config.md +0 -735
  63. package/docs/v3.0/EXECUTION-FLOW.md +0 -1304
  64. package/src/commands/utils/migrate-state.js +0 -158
  65. package/src/commands/utils/upgrade.js +0 -346
  66. package/src/lib/validators/architecture-validator.js +0 -60
  67. package/src/lib/validators/content-validator.js +0 -164
  68. package/src/lib/validators/package-validator.js +0 -61
  69. package/src/lib/validators/ui-contrast-validator.js +0 -44
  70. package/stacks/blazor-azure/.claude/commands/morph-apply.md +0 -221
  71. package/stacks/blazor-azure/.claude/commands/morph-archive.md +0 -79
  72. package/stacks/blazor-azure/.claude/commands/morph-deploy.md +0 -529
  73. package/stacks/blazor-azure/.claude/commands/morph-infra.md +0 -209
  74. package/stacks/blazor-azure/.claude/commands/morph-preflight.md +0 -227
  75. package/stacks/blazor-azure/.claude/commands/morph-proposal.md +0 -122
  76. package/stacks/blazor-azure/.claude/commands/morph-status.md +0 -86
  77. package/stacks/blazor-azure/.claude/commands/morph-troubleshoot.md +0 -122
  78. package/stacks/blazor-azure/.claude/skills/level-0-meta/README.md +0 -7
  79. package/stacks/blazor-azure/.claude/skills/level-0-meta/code-review.md +0 -226
  80. package/stacks/blazor-azure/.claude/skills/level-0-meta/morph-checklist.md +0 -117
  81. package/stacks/blazor-azure/.claude/skills/level-0-meta/simulation-checklist.md +0 -77
  82. package/stacks/blazor-azure/.claude/skills/level-1-workflows/README.md +0 -7
  83. package/stacks/blazor-azure/.claude/skills/level-1-workflows/morph-replicate.md +0 -213
  84. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-clarify.md +0 -131
  85. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-design.md +0 -213
  86. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-setup.md +0 -106
  87. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-tasks.md +0 -164
  88. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-uiux.md +0 -169
  89. package/stacks/blazor-azure/.claude/skills/level-2-domains/README.md +0 -14
  90. package/stacks/blazor-azure/.claude/skills/level-2-domains/ai-agents/ai-system-architect.md +0 -192
  91. package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/po-pm-advisor.md +0 -197
  92. package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/prompt-engineer.md +0 -189
  93. package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/seo-growth-hacker.md +0 -320
  94. package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/standards-architect.md +0 -156
  95. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/api-designer.md +0 -59
  96. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/dotnet-senior.md +0 -77
  97. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/ef-modeler.md +0 -58
  98. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/hangfire-orchestrator.md +0 -126
  99. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/ms-agent-expert.md +0 -45
  100. package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/blazor-builder.md +0 -210
  101. package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/nextjs-expert.md +0 -154
  102. package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/ui-ux-designer.md +0 -191
  103. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/azure-architect.md +0 -142
  104. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/azure-deploy-specialist.md +0 -699
  105. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/bicep-architect.md +0 -126
  106. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/container-specialist.md +0 -131
  107. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/devops-engineer.md +0 -119
  108. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/asaas-financial.md +0 -130
  109. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/azure-identity.md +0 -142
  110. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/clerk-auth.md +0 -108
  111. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/hangfire-orchestrator.md +0 -64
  112. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/resend-email.md +0 -119
  113. package/stacks/blazor-azure/.claude/skills/level-2-domains/quality/code-analyzer.md +0 -235
  114. package/stacks/blazor-azure/.claude/skills/level-2-domains/quality/testing-specialist.md +0 -126
  115. package/stacks/blazor-azure/.claude/skills/level-3-technologies/README.md +0 -7
  116. package/stacks/blazor-azure/.claude/skills/level-4-patterns/README.md +0 -7
  117. package/stacks/blazor-azure/.morph/archive/.gitkeep +0 -25
  118. package/stacks/blazor-azure/.morph/features/.gitkeep +0 -25
  119. package/stacks/blazor-azure/.morph/schemas/agent.schema.json +0 -296
  120. package/stacks/blazor-azure/.morph/schemas/tasks.schema.json +0 -220
  121. package/stacks/blazor-azure/.morph/specs/.gitkeep +0 -20
  122. package/stacks/blazor-azure/.morph/test-infra/example.bicep +0 -59
  123. package/stacks/nextjs-supabase/.claude/commands/morph-apply.md +0 -221
  124. package/stacks/nextjs-supabase/.claude/commands/morph-archive.md +0 -79
  125. package/stacks/nextjs-supabase/.claude/commands/morph-deploy.md +0 -529
  126. package/stacks/nextjs-supabase/.claude/commands/morph-infra.md +0 -209
  127. package/stacks/nextjs-supabase/.claude/commands/morph-preflight.md +0 -227
  128. package/stacks/nextjs-supabase/.claude/commands/morph-proposal.md +0 -122
  129. package/stacks/nextjs-supabase/.claude/commands/morph-status.md +0 -86
  130. package/stacks/nextjs-supabase/.claude/commands/morph-troubleshoot.md +0 -122
  131. package/stacks/nextjs-supabase/.claude/settings.local.json +0 -6
  132. package/stacks/nextjs-supabase/.claude/skills/level-2-domains/backend/dotnet-supabase.md +0 -244
  133. package/stacks/nextjs-supabase/.claude/skills/level-2-domains/frontend/nextjs-supabase.md +0 -335
  134. package/stacks/nextjs-supabase/.claude/skills/level-2-domains/infrastructure/easypanel-deployer.md +0 -189
  135. package/stacks/nextjs-supabase/.claude/skills/level-2-domains/integrations/supabase-expert.md +0 -50
  136. /package/docs/{v3.0 → next-generation}/ANALYSIS.md +0 -0
  137. /package/docs/{v3.0 → next-generation}/ARCHITECTURE.md +0 -0
  138. /package/docs/{v3.0 → next-generation}/FEATURES.md +0 -0
  139. /package/docs/{v3.0 → next-generation}/README.md +0 -0
  140. /package/docs/{v3.0 → next-generation}/ROADMAP.md +0 -0
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Analytics Engine — Metrics collection and JSONL storage
3
+ *
4
+ * Records events to append-only JSONL files:
5
+ * - .morph/analytics/threads-log.jsonl
6
+ * - .morph/analytics/context-log.jsonl
7
+ * - .morph/analytics/trust-log.jsonl
8
+ *
9
+ * Provides aggregation, ASCII chart generation, and 30-day dashboards.
10
+ * Auto-prunes entries older than 90 days.
11
+ */
12
+
13
+ import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
14
+ import { join } from 'path';
15
+
16
+ const ANALYTICS_DIR = join(process.cwd(), '.morph/analytics');
17
+ const THREADS_LOG = join(ANALYTICS_DIR, 'threads-log.jsonl');
18
+ const CONTEXT_LOG = join(ANALYTICS_DIR, 'context-log.jsonl');
19
+ const TRUST_LOG = join(ANALYTICS_DIR, 'trust-log.jsonl');
20
+ const PRUNE_DAYS = 90;
21
+ const DASHBOARD_DAYS = 30;
22
+
23
+ // ============================================================================
24
+ // Internal Helpers
25
+ // ============================================================================
26
+
27
+ function ensureAnalyticsDir() {
28
+ if (!existsSync(ANALYTICS_DIR)) {
29
+ mkdirSync(ANALYTICS_DIR, { recursive: true });
30
+ }
31
+ }
32
+
33
+ function appendJSONL(filePath, record) {
34
+ ensureAnalyticsDir();
35
+ appendFileSync(filePath, JSON.stringify(record) + '\n', 'utf8');
36
+ }
37
+
38
+ function readJSONL(filePath) {
39
+ if (!existsSync(filePath)) return [];
40
+
41
+ const lines = readFileSync(filePath, 'utf8').trim().split('\n').filter(Boolean);
42
+ const records = [];
43
+
44
+ for (const line of lines) {
45
+ try {
46
+ records.push(JSON.parse(line));
47
+ } catch {
48
+ // Skip malformed lines
49
+ }
50
+ }
51
+ return records;
52
+ }
53
+
54
+ function pruneJSONL(filePath) {
55
+ if (!existsSync(filePath)) return;
56
+
57
+ const cutoff = new Date();
58
+ cutoff.setDate(cutoff.getDate() - PRUNE_DAYS);
59
+
60
+ const records = readJSONL(filePath);
61
+ const fresh = records.filter(r => {
62
+ const ts = new Date(r.timestamp || r.createdAt || 0);
63
+ return ts > cutoff;
64
+ });
65
+
66
+ if (fresh.length < records.length) {
67
+ writeFileSync(filePath, fresh.map(r => JSON.stringify(r)).join('\n') + '\n', 'utf8');
68
+ }
69
+ }
70
+
71
+ function recentRecords(records, days = DASHBOARD_DAYS) {
72
+ const cutoff = new Date();
73
+ cutoff.setDate(cutoff.getDate() - days);
74
+ return records.filter(r => {
75
+ const ts = new Date(r.timestamp || r.createdAt || 0);
76
+ return ts > cutoff;
77
+ });
78
+ }
79
+
80
+ // ============================================================================
81
+ // Event Recording
82
+ // ============================================================================
83
+
84
+ /**
85
+ * Record a thread analytics event
86
+ * @param {Object} event
87
+ * @param {string} event.type - Event type (thread_created|thread_completed|thread_failed|checkpoint_passed|etc.)
88
+ * @param {string} event.feature - Feature name
89
+ * @param {string} [event.threadId] - Thread ID
90
+ * @param {string} [event.agent] - Agent name
91
+ * @param {Object} [event.data] - Additional event data
92
+ */
93
+ export function recordEvent(event) {
94
+ const record = {
95
+ ...event,
96
+ timestamp: new Date().toISOString()
97
+ };
98
+
99
+ // Route to appropriate log
100
+ if (event.type?.startsWith('context_') || event.type === 'token_usage') {
101
+ appendJSONL(CONTEXT_LOG, record);
102
+ } else if (event.type?.startsWith('trust_')) {
103
+ appendJSONL(TRUST_LOG, record);
104
+ } else {
105
+ appendJSONL(THREADS_LOG, record);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Record a context event (token usage, optimization, etc.)
111
+ * @param {Object} event
112
+ */
113
+ export function recordContextEvent(event) {
114
+ recordEvent({ ...event, type: event.type || 'context_usage' });
115
+ }
116
+
117
+ /**
118
+ * Record a trust level change event
119
+ * @param {string} feature
120
+ * @param {string} level - new trust level
121
+ * @param {string} reason
122
+ */
123
+ export function recordTrustEvent(feature, level, reason = '') {
124
+ appendJSONL(TRUST_LOG, {
125
+ timestamp: new Date().toISOString(),
126
+ type: 'trust_level_changed',
127
+ feature,
128
+ level,
129
+ reason
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Record auto-approval event (gate approved by trust)
135
+ * @param {string} feature
136
+ * @param {string} gate - 'design' | 'tasks' | 'proposal'
137
+ * @param {string} trustLevel
138
+ * @param {number} passRate
139
+ */
140
+ export function recordAutoApproval(feature, gate, trustLevel, passRate) {
141
+ appendJSONL(TRUST_LOG, {
142
+ timestamp: new Date().toISOString(),
143
+ type: 'trust_auto_approved',
144
+ feature,
145
+ gate,
146
+ trustLevel,
147
+ passRate,
148
+ timeSavedMinutes: 5 // estimated minutes saved per auto-approval
149
+ });
150
+ }
151
+
152
+ /**
153
+ * Get trust progression for a feature
154
+ * @param {string} featureName
155
+ * @returns {Array} Trust level history
156
+ */
157
+ export function getTrustProgression(featureName) {
158
+ const events = readJSONL(TRUST_LOG)
159
+ .filter(e => e.feature === featureName && e.type === 'trust_level_changed')
160
+ .sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
161
+ return events;
162
+ }
163
+
164
+ // ============================================================================
165
+ // Feature Analytics
166
+ // ============================================================================
167
+
168
+ /**
169
+ * Get analytics for a specific feature
170
+ * @param {string} feature - Feature name
171
+ * @returns {Object} Feature analytics
172
+ */
173
+ export function getFeatureAnalytics(feature) {
174
+ const allThreadEvents = readJSONL(THREADS_LOG).filter(r => r.feature === feature);
175
+ const allContextEvents = readJSONL(CONTEXT_LOG).filter(r => r.feature === feature);
176
+
177
+ const threadsByStatus = allThreadEvents
178
+ .filter(e => e.type === 'thread_completed' || e.type === 'thread_failed')
179
+ .reduce((acc, e) => {
180
+ const status = e.type === 'thread_completed' ? 'completed' : 'failed';
181
+ acc[status] = (acc[status] || 0) + 1;
182
+ return acc;
183
+ }, {});
184
+
185
+ const checkpoints = allThreadEvents.filter(e =>
186
+ e.type === 'checkpoint_passed' || e.type === 'checkpoint_failed'
187
+ );
188
+ const checkpointPassRate = checkpoints.length > 0
189
+ ? Math.round(checkpoints.filter(e => e.type === 'checkpoint_passed').length / checkpoints.length * 100)
190
+ : 100;
191
+
192
+ const totalDuration = allThreadEvents
193
+ .filter(e => e.type === 'thread_completed' && e.data?.duration)
194
+ .reduce((sum, e) => sum + e.data.duration, 0);
195
+
196
+ const toolCallEvents = allThreadEvents.filter(e => e.type === 'tool_call');
197
+ const tokenEvents = allContextEvents.filter(e => e.data?.tokensUsed);
198
+ const totalTokens = tokenEvents.reduce((sum, e) => sum + (e.data.tokensUsed || 0), 0);
199
+
200
+ return {
201
+ feature,
202
+ threads: threadsByStatus,
203
+ checkpointPassRate,
204
+ totalDuration,
205
+ totalToolCalls: toolCallEvents.length,
206
+ totalTokensUsed: totalTokens,
207
+ optimizationEvents: allContextEvents.filter(e => e.type === 'context_optimized').length
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Get project-wide analytics dashboard (30-day summary)
213
+ * @returns {Object} Project analytics
214
+ */
215
+ export function getProjectAnalytics() {
216
+ const allThreadEvents = recentRecords(readJSONL(THREADS_LOG));
217
+ const allContextEvents = recentRecords(readJSONL(CONTEXT_LOG));
218
+ const allTrustEvents = recentRecords(readJSONL(TRUST_LOG));
219
+
220
+ const features = [...new Set(allThreadEvents.map(e => e.feature).filter(Boolean))];
221
+
222
+ const completedThreads = allThreadEvents.filter(e => e.type === 'thread_completed');
223
+ const failedThreads = allThreadEvents.filter(e => e.type === 'thread_failed');
224
+ const checkpoints = allThreadEvents.filter(e =>
225
+ e.type === 'checkpoint_passed' || e.type === 'checkpoint_failed'
226
+ );
227
+
228
+ const parallelEvents = allThreadEvents.filter(e => e.data?.type === 'parallel');
229
+ const avgParallel = parallelEvents.length > 0
230
+ ? parallelEvents.reduce((sum, e) => sum + (e.data?.concurrency || 1), 0) / parallelEvents.length
231
+ : 1;
232
+
233
+ const autoApprovalEvents = allTrustEvents.filter(e => e.type === 'trust_auto_approved');
234
+ const avgTokens = allContextEvents.filter(e => e.data?.tokensUsed).length > 0
235
+ ? Math.round(allContextEvents.reduce((sum, e) => sum + (e.data?.tokensUsed || 0), 0) / allContextEvents.filter(e => e.data?.tokensUsed).length)
236
+ : 0;
237
+
238
+ return {
239
+ period: `${DASHBOARD_DAYS} days`,
240
+ features: features.length,
241
+ threads: {
242
+ total: completedThreads.length + failedThreads.length,
243
+ completed: completedThreads.length,
244
+ failed: failedThreads.length
245
+ },
246
+ checkpoints: {
247
+ total: checkpoints.length,
248
+ passed: checkpoints.filter(e => e.type === 'checkpoint_passed').length,
249
+ passRate: checkpoints.length > 0
250
+ ? Math.round(checkpoints.filter(e => e.type === 'checkpoint_passed').length / checkpoints.length * 100)
251
+ : 100
252
+ },
253
+ parallelization: {
254
+ avgConcurrency: Math.round(avgParallel * 10) / 10,
255
+ parallelThreads: parallelEvents.length
256
+ },
257
+ trust: {
258
+ autoApprovals: autoApprovalEvents.length,
259
+ trustChanges: allTrustEvents.filter(e => e.type === 'trust_level_changed').length
260
+ },
261
+ context: {
262
+ avgTokensPerSession: avgTokens,
263
+ optimizations: allContextEvents.filter(e => e.type === 'context_optimized').length
264
+ }
265
+ };
266
+ }
267
+
268
+ // ============================================================================
269
+ // ASCII Charts
270
+ // ============================================================================
271
+
272
+ /**
273
+ * Generate an ASCII bar chart
274
+ * @param {Object} data - { label: value }
275
+ * @param {Object} [opts]
276
+ * @param {number} [opts.width=40] - Chart width in chars
277
+ * @param {string} [opts.title] - Chart title
278
+ * @returns {string} ASCII chart string
279
+ */
280
+ export function generateAsciiChart(data, { width = 40, title = '' } = {}) {
281
+ const entries = Object.entries(data);
282
+ if (entries.length === 0) return ' (no data)';
283
+
284
+ const maxValue = Math.max(...entries.map(([, v]) => v), 1);
285
+ const maxLabelLen = Math.max(...entries.map(([k]) => k.length));
286
+ const lines = [];
287
+
288
+ if (title) {
289
+ lines.push(` ${title}`);
290
+ lines.push(' ' + '─'.repeat(width + maxLabelLen + 5));
291
+ }
292
+
293
+ for (const [label, value] of entries) {
294
+ const barLen = Math.round((value / maxValue) * width);
295
+ const bar = '█'.repeat(barLen);
296
+ const paddedLabel = label.padStart(maxLabelLen);
297
+ lines.push(` ${paddedLabel} │${bar} ${value}`);
298
+ }
299
+
300
+ return lines.join('\n');
301
+ }
302
+
303
+ /**
304
+ * Generate thread timeline ASCII chart
305
+ * @param {Array} events - Array of thread events with timestamps
306
+ * @param {string} label - Chart label
307
+ * @returns {string} ASCII timeline
308
+ */
309
+ export function generateTimelineChart(events, label = 'Timeline') {
310
+ if (events.length === 0) return ' (no data)';
311
+
312
+ const now = new Date();
313
+ const days = Array.from({ length: 7 }, (_, i) => {
314
+ const d = new Date(now);
315
+ d.setDate(d.getDate() - (6 - i));
316
+ return d.toLocaleDateString('en-US', { weekday: 'short' });
317
+ });
318
+
319
+ const counts = new Array(7).fill(0);
320
+ for (const event of events) {
321
+ const ts = new Date(event.timestamp);
322
+ const daysAgo = Math.floor((now - ts) / (1000 * 60 * 60 * 24));
323
+ if (daysAgo < 7) {
324
+ counts[6 - daysAgo]++;
325
+ }
326
+ }
327
+
328
+ const data = {};
329
+ days.forEach((d, i) => { data[d] = counts[i]; });
330
+
331
+ return generateAsciiChart(data, { title: label });
332
+ }
333
+
334
+ // ============================================================================
335
+ // Maintenance
336
+ // ============================================================================
337
+
338
+ /**
339
+ * Prune all analytics logs (remove entries older than 90 days)
340
+ */
341
+ export function pruneAnalytics() {
342
+ pruneJSONL(THREADS_LOG);
343
+ pruneJSONL(CONTEXT_LOG);
344
+ pruneJSONL(TRUST_LOG);
345
+ }