UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

184 lines 7.16 kB
/** * Artifact Metrics — record and query per-artifact token metrics * * Hooks into artifact save events, records path/type/agent/timestamp/tokens, * and persists to `.aiwg/metrics/tokens/`. * * @module metrics/artifact-metrics * @issue #173 * @schema @agentic/code/frameworks/sdlc-complete/schemas/flows/token-efficiency.yaml */ import { promises as fs } from 'fs'; import path from 'path'; import { countTokens, evaluateThreshold, BENCHMARK_TOKENS_PER_LINE, BASELINE_TOKENS_PER_LINE, } from './token-counter.js'; // ============================================================================ // Constants // ============================================================================ const METRICS_DIR = '.aiwg/metrics/tokens'; const RECORDS_FILE = 'records.json'; // ============================================================================ // ArtifactMetricsStore // ============================================================================ export class ArtifactMetricsStore { projectPath; metricsDir; records; constructor(projectPath) { this.projectPath = projectPath; this.metricsDir = path.join(projectPath, METRICS_DIR); this.records = []; } /** * Record metrics for an artifact after save. */ async recordArtifact(artifactPath, content, artifactType, agent) { const tokenCount = countTokens(content); const threshold = evaluateThreshold(tokenCount.tokensPerLine); const relativePath = path.isAbsolute(artifactPath) ? path.relative(this.projectPath, artifactPath) : artifactPath; const record = { artifactPath: relativePath, artifactType, agent, timestamp: new Date().toISOString(), tokens: { totalTokens: tokenCount.tokens, nonBlankLines: tokenCount.nonBlankLines, tokensPerLine: tokenCount.tokensPerLine, characters: tokenCount.characters, }, thresholdStatus: threshold.level, }; this.records.push(record); await this.persist(); return record; } /** * Query records with optional filters. */ queryRecords(options = {}) { let filtered = [...this.records]; if (options.agent) { const agentLower = options.agent.toLowerCase(); filtered = filtered.filter((r) => r.agent.toLowerCase() === agentLower); } if (options.since) { const sinceDate = parseSinceDate(options.since); if (sinceDate) { filtered = filtered.filter((r) => new Date(r.timestamp) >= sinceDate); } } return filtered; } /** * Get per-agent efficiency summaries. */ getAgentSummaries(options = {}) { const records = this.queryRecords(options); const byAgent = new Map(); for (const record of records) { const existing = byAgent.get(record.agent) || []; existing.push(record); byAgent.set(record.agent, existing); } const summaries = []; for (const [agentName, agentRecords] of byAgent) { const totalTokens = agentRecords.reduce((sum, r) => sum + r.tokens.totalTokens, 0); const totalLines = agentRecords.reduce((sum, r) => sum + r.tokens.nonBlankLines, 0); const avgTokensPerLine = totalLines > 0 ? totalTokens / totalLines : 0; const tokensPerLineValues = agentRecords.map((r) => r.tokens.tokensPerLine); const minTokensPerLine = Math.min(...tokensPerLineValues); const maxTokensPerLine = Math.max(...tokensPerLineValues); const vsBenchmark = avgTokensPerLine > 0 ? ((avgTokensPerLine - BENCHMARK_TOKENS_PER_LINE) / BENCHMARK_TOKENS_PER_LINE) * 100 : 0; const vsBaseline = avgTokensPerLine > 0 ? ((avgTokensPerLine - BASELINE_TOKENS_PER_LINE) / BASELINE_TOKENS_PER_LINE) * 100 : 0; const threshold = evaluateThreshold(avgTokensPerLine); const trend = calculateTrend(agentRecords); summaries.push({ agentName, period: options.since || 'all-time', totalArtifacts: agentRecords.length, totalTokens, totalLines, avgTokensPerLine: Math.round(avgTokensPerLine * 100) / 100, minTokensPerLine: Math.round(minTokensPerLine * 100) / 100, maxTokensPerLine: Math.round(maxTokensPerLine * 100) / 100, vsBenchmark: Math.round(vsBenchmark * 100) / 100, vsBaseline: Math.round(vsBaseline * 100) / 100, trend, thresholdStatus: threshold.level, }); } return summaries; } /** * Load records from disk. */ async load() { try { const filePath = path.join(this.metricsDir, RECORDS_FILE); const content = await fs.readFile(filePath, 'utf-8'); this.records = JSON.parse(content); } catch { this.records = []; } } /** * Persist records to disk. */ async persist() { try { await fs.mkdir(this.metricsDir, { recursive: true }); const filePath = path.join(this.metricsDir, RECORDS_FILE); await fs.writeFile(filePath, JSON.stringify(this.records, null, 2), 'utf-8'); } catch { // Silently fail persistence — metrics are non-critical } } /** * Get all records (for testing). */ getAllRecords() { return [...this.records]; } } // ============================================================================ // Helpers // ============================================================================ function parseSinceDate(since) { const durationMatch = since.match(/^(\d+)d$/); if (durationMatch) { const days = parseInt(durationMatch[1], 10); const date = new Date(); date.setDate(date.getDate() - days); return date; } const parsed = new Date(since); if (!isNaN(parsed.getTime())) { return parsed; } return null; } function calculateTrend(records) { if (records.length < 4) return 'stable'; const sorted = [...records].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); const midpoint = Math.floor(sorted.length / 2); const firstHalf = sorted.slice(0, midpoint); const secondHalf = sorted.slice(midpoint); const avgFirst = firstHalf.reduce((sum, r) => sum + r.tokens.tokensPerLine, 0) / firstHalf.length; const avgSecond = secondHalf.reduce((sum, r) => sum + r.tokens.tokensPerLine, 0) / secondHalf.length; const changePercent = ((avgSecond - avgFirst) / avgFirst) * 100; if (changePercent < -5) return 'improving'; if (changePercent > 5) return 'degrading'; return 'stable'; } //# sourceMappingURL=artifact-metrics.js.map