// telemetry.dart — Pillar 6: Opt-in local telemetry & feedback loop import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as p; import 'logger.dart'; class Telemetry { /// Record a generation event (local-only, opt-in) static Future record({ required String projectName, required String outputDir, required List templateFiles, }) async { final telemetryPath = p.join(outputDir, 'telemetry.json'); Map data; try { final file = File(telemetryPath); data = file.existsSync() ? jsonDecode(await file.readAsString()) as Map : _emptyData(projectName); } catch (_) { data = _emptyData(projectName); } final generations = data['generations'] as List; generations.add({ 'timestamp': DateTime.now().toIso8601String(), 'templateCount': templateFiles.length, 'templates': templateFiles, }); // Keep only last 100 events if (generations.length > 100) { data['generations'] = generations.sublist(generations.length - 100); } data['lastGeneratedAt'] = DateTime.now().toIso8601String(); data['totalGenerations'] = (data['totalGenerations'] as int? ?? 0) + 1; final file = File(telemetryPath); await file.parent.create(recursive: true); await file.writeAsString(const JsonEncoder.withIndent(' ').convert(data)); } /// Print the telemetry report static Future printReport({required String outputDir}) async { final file = File(p.join(outputDir, 'telemetry.json')); if (!file.existsSync()) { Logger.warn( 'No telemetry data found. Ensure telemetry_opt_in: true in project-brief.yaml'); return; } final data = jsonDecode(await file.readAsString()) as Map; Logger.info('\nšŸ“Š Telemetry Report — ${data['projectName']}'); Logger.info('─' * 42); Logger.info('Total generations: ${data['totalGenerations']}'); Logger.info('Last generated: ${data['lastGeneratedAt']}'); final generations = data['generations'] as List; if (generations.isNotEmpty) { final last = generations.last as Map; final templates = (last['templates'] as List).cast(); Logger.info('\nLast generation used ${templates.length} templates:'); final ruleCount = templates.where((t) => t.startsWith('rules/')).length; final agentCount = templates.where((t) => t.startsWith('agents/')).length; final skillCount = templates.where((t) => t.startsWith('skills/')).length; Logger.dim( ' Rules: $ruleCount | Agents: $agentCount | Skills: $skillCount'); } Logger.info('\nšŸ’” Quarterly template quality review checklist:'); Logger.dim( ' ā–” Review AI code review comments — which rules are most violated?'); Logger.dim( ' ā–” Ask teams: which agents are actually consulted vs. ignored?'); Logger.dim( ' ā–” Check if generated rules reduced hallucinations vs. last quarter'); Logger.dim( ' ā–” Identify brief combinations that produce the most AI errors'); Logger.dim( ' ā–” Update templates based on feedback, bump version in CHANGELOG.md'); } static Map _emptyData(String projectName) => { 'projectName': projectName, 'totalGenerations': 0, 'lastGeneratedAt': '', 'generations': [], 'note': 'Local-only telemetry. Never sent anywhere. opt-in via telemetry_opt_in: true.', }; }