Files
everything-claude-code/scripts/lib/skill-evolution/health.js
Aryan Tejani 89044e8c33 feat(design): skill health dashboard mockup (#518)
* feat(Design): skill health dashboard mockup

* fix(comments): code according to comments
2026-03-16 14:01:41 -07:00

264 lines
7.4 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const provenance = require('./provenance');
const tracker = require('./tracker');
const versioning = require('./versioning');
const DAY_IN_MS = 24 * 60 * 60 * 1000;
const PENDING_AMENDMENT_STATUSES = Object.freeze(new Set(['pending', 'proposed', 'queued', 'open']));
function roundRate(value) {
if (value === null) {
return null;
}
return Math.round(value * 10000) / 10000;
}
function formatRate(value) {
if (value === null) {
return 'n/a';
}
return `${Math.round(value * 100)}%`;
}
function summarizeHealthReport(report) {
const totalSkills = report.skills.length;
const decliningSkills = report.skills.filter(skill => skill.declining).length;
const healthySkills = totalSkills - decliningSkills;
return {
total_skills: totalSkills,
healthy_skills: healthySkills,
declining_skills: decliningSkills,
};
}
function listSkillsInRoot(rootPath) {
if (!rootPath || !fs.existsSync(rootPath)) {
return [];
}
return fs.readdirSync(rootPath, { withFileTypes: true })
.filter(entry => entry.isDirectory())
.map(entry => ({
skill_id: entry.name,
skill_dir: path.join(rootPath, entry.name),
}))
.filter(entry => fs.existsSync(path.join(entry.skill_dir, 'SKILL.md')));
}
function discoverSkills(options = {}) {
const roots = provenance.getSkillRoots(options);
const discoveredSkills = [
...listSkillsInRoot(options.skillsRoot || roots.curated).map(skill => ({
...skill,
skill_type: provenance.SKILL_TYPES.CURATED,
})),
...listSkillsInRoot(options.learnedRoot || roots.learned).map(skill => ({
...skill,
skill_type: provenance.SKILL_TYPES.LEARNED,
})),
...listSkillsInRoot(options.importedRoot || roots.imported).map(skill => ({
...skill,
skill_type: provenance.SKILL_TYPES.IMPORTED,
})),
];
return discoveredSkills.reduce((skillsById, skill) => {
if (!skillsById.has(skill.skill_id)) {
skillsById.set(skill.skill_id, skill);
}
return skillsById;
}, new Map());
}
function calculateSuccessRate(records) {
if (records.length === 0) {
return null;
}
const successfulRecords = records.filter(record => record.outcome === 'success').length;
return roundRate(successfulRecords / records.length);
}
function filterRecordsWithinDays(records, nowMs, days) {
const cutoff = nowMs - (days * DAY_IN_MS);
return records.filter(record => {
const recordedAtMs = Date.parse(record.recorded_at);
return !Number.isNaN(recordedAtMs) && recordedAtMs >= cutoff && recordedAtMs <= nowMs;
});
}
function getFailureTrend(successRate7d, successRate30d, warnThreshold) {
if (successRate7d === null || successRate30d === null) {
return 'stable';
}
const delta = roundRate(successRate7d - successRate30d);
if (delta <= (-1 * warnThreshold)) {
return 'worsening';
}
if (delta >= warnThreshold) {
return 'improving';
}
return 'stable';
}
function countPendingAmendments(skillDir) {
if (!skillDir) {
return 0;
}
return versioning.getEvolutionLog(skillDir, 'amendments')
.filter(entry => {
if (typeof entry.status === 'string') {
return PENDING_AMENDMENT_STATUSES.has(entry.status);
}
return entry.event === 'proposal';
})
.length;
}
function getLastRun(records) {
if (records.length === 0) {
return null;
}
return records
.map(record => ({
timestamp: record.recorded_at,
timeMs: Date.parse(record.recorded_at),
}))
.filter(entry => !Number.isNaN(entry.timeMs))
.sort((left, right) => left.timeMs - right.timeMs)
.at(-1)?.timestamp || null;
}
function collectSkillHealth(options = {}) {
const now = options.now || new Date().toISOString();
const nowMs = Date.parse(now);
if (Number.isNaN(nowMs)) {
throw new Error(`Invalid now timestamp: ${now}`);
}
const warnThreshold = typeof options.warnThreshold === 'number'
? options.warnThreshold
: Number(options.warnThreshold || 0.1);
if (!Number.isFinite(warnThreshold) || warnThreshold < 0) {
throw new Error(`Invalid warn threshold: ${options.warnThreshold}`);
}
const records = tracker.readSkillExecutionRecords(options);
const skillsById = discoverSkills(options);
const recordsBySkill = records.reduce((groupedRecords, record) => {
if (!groupedRecords.has(record.skill_id)) {
groupedRecords.set(record.skill_id, []);
}
groupedRecords.get(record.skill_id).push(record);
return groupedRecords;
}, new Map());
for (const skillId of recordsBySkill.keys()) {
if (!skillsById.has(skillId)) {
skillsById.set(skillId, {
skill_id: skillId,
skill_dir: null,
skill_type: provenance.SKILL_TYPES.UNKNOWN,
});
}
}
const skills = Array.from(skillsById.values())
.sort((left, right) => left.skill_id.localeCompare(right.skill_id))
.map(skill => {
const skillRecords = recordsBySkill.get(skill.skill_id) || [];
const records7d = filterRecordsWithinDays(skillRecords, nowMs, 7);
const records30d = filterRecordsWithinDays(skillRecords, nowMs, 30);
const successRate7d = calculateSuccessRate(records7d);
const successRate30d = calculateSuccessRate(records30d);
const currentVersionNumber = skill.skill_dir ? versioning.getCurrentVersion(skill.skill_dir) : 0;
const failureTrend = getFailureTrend(successRate7d, successRate30d, warnThreshold);
return {
skill_id: skill.skill_id,
skill_type: skill.skill_type,
current_version: currentVersionNumber > 0 ? `v${currentVersionNumber}` : null,
pending_amendments: countPendingAmendments(skill.skill_dir),
success_rate_7d: successRate7d,
success_rate_30d: successRate30d,
failure_trend: failureTrend,
declining: failureTrend === 'worsening',
last_run: getLastRun(skillRecords),
run_count_7d: records7d.length,
run_count_30d: records30d.length,
};
});
return {
generated_at: now,
warn_threshold: warnThreshold,
skills,
};
}
function formatHealthReport(report, options = {}) {
if (options.json) {
return `${JSON.stringify(report, null, 2)}\n`;
}
const summary = summarizeHealthReport(report);
if (!report.skills.length) {
return [
'ECC skill health',
`Generated: ${report.generated_at}`,
'',
'No skill execution records found.',
'',
].join('\n');
}
const lines = [
'ECC skill health',
`Generated: ${report.generated_at}`,
`Skills: ${summary.total_skills} total, ${summary.healthy_skills} healthy, ${summary.declining_skills} declining`,
'',
'skill version 7d 30d trend pending last run',
'--------------------------------------------------------------------------',
];
for (const skill of report.skills) {
const statusLabel = skill.declining ? '!' : ' ';
lines.push([
`${statusLabel}${skill.skill_id}`.padEnd(16),
String(skill.current_version || '-').padEnd(9),
formatRate(skill.success_rate_7d).padEnd(6),
formatRate(skill.success_rate_30d).padEnd(6),
skill.failure_trend.padEnd(11),
String(skill.pending_amendments).padEnd(9),
skill.last_run || '-',
].join(' '));
}
return `${lines.join('\n')}\n`;
}
module.exports = {
PENDING_AMENDMENT_STATUSES,
calculateSuccessRate,
collectSkillHealth,
discoverSkills,
filterRecordsWithinDays,
formatHealthReport,
summarizeHealthReport,
};