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
JavaScript
/**
* 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