mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 21:53:28 +08:00
* feat(Design): skill health dashboard mockup * fix(comments): code according to comments
264 lines
7.4 KiB
JavaScript
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,
|
|
};
|