@devran-ai/kit 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/CheatSheet.md +350 -0
- package/.agent/README.md +76 -0
- package/.agent/agents/README.md +155 -0
- package/.agent/agents/architect.md +185 -0
- package/.agent/agents/backend-specialist.md +276 -0
- package/.agent/agents/build-error-resolver.md +207 -0
- package/.agent/agents/code-reviewer.md +162 -0
- package/.agent/agents/database-architect.md +138 -0
- package/.agent/agents/devops-engineer.md +144 -0
- package/.agent/agents/doc-updater.md +229 -0
- package/.agent/agents/e2e-runner.md +145 -0
- package/.agent/agents/explorer-agent.md +143 -0
- package/.agent/agents/frontend-specialist.md +144 -0
- package/.agent/agents/go-reviewer.md +128 -0
- package/.agent/agents/knowledge-agent.md +197 -0
- package/.agent/agents/mobile-developer.md +150 -0
- package/.agent/agents/performance-optimizer.md +175 -0
- package/.agent/agents/planner.md +133 -0
- package/.agent/agents/pr-reviewer.md +148 -0
- package/.agent/agents/python-reviewer.md +123 -0
- package/.agent/agents/refactor-cleaner.md +201 -0
- package/.agent/agents/reliability-engineer.md +156 -0
- package/.agent/agents/security-reviewer.md +141 -0
- package/.agent/agents/sprint-orchestrator.md +124 -0
- package/.agent/agents/tdd-guide.md +179 -0
- package/.agent/agents/typescript-reviewer.md +110 -0
- package/.agent/checklists/README.md +102 -0
- package/.agent/checklists/pre-commit.md +93 -0
- package/.agent/checklists/session-end.md +99 -0
- package/.agent/checklists/session-start.md +102 -0
- package/.agent/checklists/task-complete.md +81 -0
- package/.agent/commands/README.md +130 -0
- package/.agent/commands/adr.md +29 -0
- package/.agent/commands/ask.md +28 -0
- package/.agent/commands/build.md +30 -0
- package/.agent/commands/changelog.md +40 -0
- package/.agent/commands/checkpoint.md +28 -0
- package/.agent/commands/code-review.md +65 -0
- package/.agent/commands/compact.md +28 -0
- package/.agent/commands/cook.md +30 -0
- package/.agent/commands/db.md +30 -0
- package/.agent/commands/debug.md +31 -0
- package/.agent/commands/deploy.md +37 -0
- package/.agent/commands/design.md +29 -0
- package/.agent/commands/doc.md +30 -0
- package/.agent/commands/eval.md +30 -0
- package/.agent/commands/fix.md +32 -0
- package/.agent/commands/git.md +32 -0
- package/.agent/commands/help.md +273 -0
- package/.agent/commands/implement.md +30 -0
- package/.agent/commands/integrate.md +32 -0
- package/.agent/commands/learn.md +29 -0
- package/.agent/commands/perf.md +31 -0
- package/.agent/commands/plan.md +56 -0
- package/.agent/commands/pr-describe.md +65 -0
- package/.agent/commands/pr-fix.md +45 -0
- package/.agent/commands/pr-merge.md +45 -0
- package/.agent/commands/pr-review.md +50 -0
- package/.agent/commands/pr-split.md +54 -0
- package/.agent/commands/pr-status.md +56 -0
- package/.agent/commands/pr.md +58 -0
- package/.agent/commands/refactor.md +32 -0
- package/.agent/commands/research.md +28 -0
- package/.agent/commands/scout.md +30 -0
- package/.agent/commands/security-scan.md +33 -0
- package/.agent/commands/setup.md +31 -0
- package/.agent/commands/status.md +59 -0
- package/.agent/commands/tdd.md +73 -0
- package/.agent/commands/verify.md +58 -0
- package/.agent/contexts/brainstorm.md +26 -0
- package/.agent/contexts/debug.md +28 -0
- package/.agent/contexts/implement.md +29 -0
- package/.agent/contexts/plan-quality-log.md +30 -0
- package/.agent/contexts/review.md +27 -0
- package/.agent/contexts/ship.md +28 -0
- package/.agent/decisions/001-trust-grade-governance.md +46 -0
- package/.agent/decisions/002-cross-ide-generation.md +15 -0
- package/.agent/engine/identity.json +4 -0
- package/.agent/engine/loading-rules.json +193 -0
- package/.agent/engine/marketplace-index.json +29 -0
- package/.agent/engine/mcp-servers/filesystem.json +9 -0
- package/.agent/engine/mcp-servers/github.json +11 -0
- package/.agent/engine/mcp-servers/postgres.json +11 -0
- package/.agent/engine/mcp-servers/supabase.json +11 -0
- package/.agent/engine/mcp-servers/vercel.json +11 -0
- package/.agent/engine/reliability-config.json +14 -0
- package/.agent/engine/sdlc-map.json +50 -0
- package/.agent/engine/workflow-state.json +167 -0
- package/.agent/hooks/README.md +101 -0
- package/.agent/hooks/hooks.json +104 -0
- package/.agent/hooks/templates/session-end.md +110 -0
- package/.agent/hooks/templates/session-start.md +95 -0
- package/.agent/manifest.json +466 -0
- package/.agent/rules/agent-upgrade-policy.md +56 -0
- package/.agent/rules/architecture.md +111 -0
- package/.agent/rules/coding-style.md +75 -0
- package/.agent/rules/documentation.md +74 -0
- package/.agent/rules/git-workflow.md +140 -0
- package/.agent/rules/quality-gate.md +117 -0
- package/.agent/rules/security.md +67 -0
- package/.agent/rules/sprint-tracking.md +103 -0
- package/.agent/rules/testing.md +80 -0
- package/.agent/rules/workflow-standards.md +30 -0
- package/.agent/rules.md +293 -0
- package/.agent/session-context.md +69 -0
- package/.agent/session-state.json +27 -0
- package/.agent/skills/README.md +135 -0
- package/.agent/skills/api-patterns/SKILL.md +117 -0
- package/.agent/skills/app-builder/SKILL.md +202 -0
- package/.agent/skills/architecture/SKILL.md +101 -0
- package/.agent/skills/behavioral-modes/SKILL.md +295 -0
- package/.agent/skills/brainstorming/SKILL.md +156 -0
- package/.agent/skills/clean-code/SKILL.md +142 -0
- package/.agent/skills/context-budget/SKILL.md +78 -0
- package/.agent/skills/continuous-learning/SKILL.md +145 -0
- package/.agent/skills/database-design/SKILL.md +303 -0
- package/.agent/skills/debugging-strategies/SKILL.md +158 -0
- package/.agent/skills/deployment-procedures/SKILL.md +191 -0
- package/.agent/skills/docker-patterns/SKILL.md +161 -0
- package/.agent/skills/eval-harness/SKILL.md +89 -0
- package/.agent/skills/frontend-patterns/SKILL.md +141 -0
- package/.agent/skills/git-workflow/SKILL.md +159 -0
- package/.agent/skills/i18n-localization/SKILL.md +191 -0
- package/.agent/skills/intelligent-routing/SKILL.md +180 -0
- package/.agent/skills/mcp-integration/SKILL.md +240 -0
- package/.agent/skills/mobile-design/SKILL.md +191 -0
- package/.agent/skills/nodejs-patterns/SKILL.md +164 -0
- package/.agent/skills/parallel-agents/SKILL.md +200 -0
- package/.agent/skills/performance-profiling/SKILL.md +134 -0
- package/.agent/skills/plan-validation/SKILL.md +192 -0
- package/.agent/skills/plan-writing/SKILL.md +183 -0
- package/.agent/skills/plan-writing/domain-enhancers.md +184 -0
- package/.agent/skills/plan-writing/plan-retrospective.md +116 -0
- package/.agent/skills/plan-writing/plan-schema.md +119 -0
- package/.agent/skills/pr-toolkit/SKILL.md +174 -0
- package/.agent/skills/production-readiness/SKILL.md +126 -0
- package/.agent/skills/security-practices/SKILL.md +109 -0
- package/.agent/skills/shell-conventions/SKILL.md +92 -0
- package/.agent/skills/strategic-compact/SKILL.md +62 -0
- package/.agent/skills/testing-patterns/SKILL.md +141 -0
- package/.agent/skills/typescript-expert/SKILL.md +160 -0
- package/.agent/skills/ui-ux-pro-max/SKILL.md +137 -0
- package/.agent/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.agent/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.agent/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/.agent/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.agent/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.agent/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.agent/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/.agent/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.agent/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/.agent/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.agent/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/.agent/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/.agent/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/.agent/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/.agent/skills/verification-loop/SKILL.md +89 -0
- package/.agent/skills/webapp-testing/SKILL.md +175 -0
- package/.agent/templates/adr-template.md +32 -0
- package/.agent/templates/bug-report.md +37 -0
- package/.agent/templates/feature-request.md +32 -0
- package/.agent/workflows/README.md +101 -0
- package/.agent/workflows/brainstorm.md +86 -0
- package/.agent/workflows/create.md +85 -0
- package/.agent/workflows/debug.md +83 -0
- package/.agent/workflows/deploy.md +114 -0
- package/.agent/workflows/enhance.md +85 -0
- package/.agent/workflows/orchestrate.md +106 -0
- package/.agent/workflows/plan.md +105 -0
- package/.agent/workflows/pr-fix.md +163 -0
- package/.agent/workflows/pr-merge.md +117 -0
- package/.agent/workflows/pr-review.md +178 -0
- package/.agent/workflows/pr-split.md +118 -0
- package/.agent/workflows/pr.md +184 -0
- package/.agent/workflows/preflight.md +107 -0
- package/.agent/workflows/preview.md +95 -0
- package/.agent/workflows/quality-gate.md +103 -0
- package/.agent/workflows/retrospective.md +100 -0
- package/.agent/workflows/review.md +104 -0
- package/.agent/workflows/status.md +89 -0
- package/.agent/workflows/test.md +98 -0
- package/.agent/workflows/ui-ux-pro-max.md +93 -0
- package/.agent/workflows/upgrade.md +97 -0
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/bin/kit.js +773 -0
- package/lib/agent-registry.js +228 -0
- package/lib/agent-reputation.js +343 -0
- package/lib/circuit-breaker.js +195 -0
- package/lib/cli-commands.js +322 -0
- package/lib/config-validator.js +274 -0
- package/lib/conflict-detector.js +252 -0
- package/lib/constants.js +47 -0
- package/lib/engineering-manager.js +336 -0
- package/lib/error-budget.js +370 -0
- package/lib/hook-system.js +256 -0
- package/lib/ide-generator.js +434 -0
- package/lib/identity.js +240 -0
- package/lib/io.js +146 -0
- package/lib/learning-engine.js +163 -0
- package/lib/loading-engine.js +421 -0
- package/lib/logger.js +118 -0
- package/lib/marketplace.js +321 -0
- package/lib/plugin-system.js +604 -0
- package/lib/plugin-verifier.js +197 -0
- package/lib/rate-limiter.js +113 -0
- package/lib/security-scanner.js +312 -0
- package/lib/self-healing.js +468 -0
- package/lib/session-manager.js +264 -0
- package/lib/skill-sandbox.js +244 -0
- package/lib/task-governance.js +522 -0
- package/lib/task-model.js +332 -0
- package/lib/updater.js +240 -0
- package/lib/verify.js +279 -0
- package/lib/workflow-engine.js +373 -0
- package/lib/workflow-events.js +166 -0
- package/lib/workflow-persistence.js +160 -0
- package/package.json +57 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devran AI Kit — Autonomous Engineering Manager
|
|
3
|
+
*
|
|
4
|
+
* Data engine for sprint planning, task auto-assignment,
|
|
5
|
+
* and velocity metrics. Powers the sprint-orchestrator agent.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/engineering-manager
|
|
8
|
+
* @author Emre Dursun
|
|
9
|
+
* @since v3.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
const taskModel = require('./task-model');
|
|
18
|
+
const agentRegistry = require('./agent-registry');
|
|
19
|
+
const agentReputation = require('./agent-reputation');
|
|
20
|
+
|
|
21
|
+
const { AGENT_DIR, ENGINE_DIR } = require('./constants');
|
|
22
|
+
const { writeJsonAtomic } = require('./io');
|
|
23
|
+
const SPRINT_FILE = 'sprint-plans.json';
|
|
24
|
+
|
|
25
|
+
/** Maximum tasks per sprint suggestion */
|
|
26
|
+
const MAX_SPRINT_SIZE = 20;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} SprintPlan
|
|
30
|
+
* @property {string} id - Sprint plan ID
|
|
31
|
+
* @property {string} name - Sprint name
|
|
32
|
+
* @property {string} createdAt - ISO timestamp
|
|
33
|
+
* @property {object[]} assignments - Task assignments
|
|
34
|
+
* @property {string} status - Plan status: 'draft' | 'active' | 'completed'
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {object} TaskAssignment
|
|
39
|
+
* @property {string} taskId - Task ID
|
|
40
|
+
* @property {string} taskTitle - Task title
|
|
41
|
+
* @property {string} suggestedAgent - Recommended agent
|
|
42
|
+
* @property {string} reason - Why this agent was chosen
|
|
43
|
+
* @property {string} priority - Task priority
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolves the sprint plans file path.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} projectRoot - Root directory
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
function resolveSprintPath(projectRoot) {
|
|
53
|
+
return path.join(projectRoot, AGENT_DIR, ENGINE_DIR, SPRINT_FILE);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Loads sprint plans from disk.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} projectRoot - Root directory
|
|
60
|
+
* @returns {{ plans: SprintPlan[] }}
|
|
61
|
+
*/
|
|
62
|
+
function loadSprintData(projectRoot) {
|
|
63
|
+
const filePath = resolveSprintPath(projectRoot);
|
|
64
|
+
|
|
65
|
+
if (!fs.existsSync(filePath)) {
|
|
66
|
+
return { plans: [] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
71
|
+
} catch {
|
|
72
|
+
return { plans: [] };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Writes sprint data atomically.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} projectRoot - Root directory
|
|
80
|
+
* @param {{ plans: SprintPlan[] }} data
|
|
81
|
+
* @returns {void}
|
|
82
|
+
*/
|
|
83
|
+
function writeSprintData(projectRoot, data) {
|
|
84
|
+
const filePath = resolveSprintPath(projectRoot);
|
|
85
|
+
writeJsonAtomic(filePath, data);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Gets the current workload (in-progress task count) for an agent.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} projectRoot - Root directory
|
|
92
|
+
* @param {string} agentName - Agent name
|
|
93
|
+
* @returns {number}
|
|
94
|
+
*/
|
|
95
|
+
function getAgentWorkload(projectRoot, agentName) {
|
|
96
|
+
try {
|
|
97
|
+
const inProgressTasks = taskModel.listTasks(projectRoot, {
|
|
98
|
+
status: 'in-progress',
|
|
99
|
+
assignee: agentName,
|
|
100
|
+
});
|
|
101
|
+
return inProgressTasks.length;
|
|
102
|
+
} catch {
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Finds the best agent for a task based on domain, reputation, and workload.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} projectRoot - Root directory
|
|
111
|
+
* @param {string} taskTitle - Task title for domain matching
|
|
112
|
+
* @param {string} taskPriority - Task priority
|
|
113
|
+
* @returns {{ agent: string, reason: string }}
|
|
114
|
+
*/
|
|
115
|
+
function findBestAgent(projectRoot, taskTitle, taskPriority) {
|
|
116
|
+
let agents = [];
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const registry = agentRegistry.loadRegistry(projectRoot);
|
|
120
|
+
agents = registry.agents;
|
|
121
|
+
} catch {
|
|
122
|
+
return { agent: 'unassigned', reason: 'No agent registry available' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (agents.length === 0) {
|
|
126
|
+
return { agent: 'unassigned', reason: 'No agents registered' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Score each agent
|
|
130
|
+
const scored = agents.map((agent) => {
|
|
131
|
+
let score = 0;
|
|
132
|
+
let reasons = [];
|
|
133
|
+
|
|
134
|
+
// 1. Domain match (keyword overlap between task title and agent domain)
|
|
135
|
+
const titleWords = taskTitle.toLowerCase().split(/\W+/);
|
|
136
|
+
const domainWords = (agent.domain || '').toLowerCase().split(/\W+/);
|
|
137
|
+
const domainOverlap = titleWords.filter((word) =>
|
|
138
|
+
word.length > 2 && domainWords.some((dw) => dw.includes(word) || word.includes(dw))
|
|
139
|
+
).length;
|
|
140
|
+
|
|
141
|
+
if (domainOverlap > 0) {
|
|
142
|
+
score += domainOverlap * 30;
|
|
143
|
+
reasons.push(`domain match (${domainOverlap} keywords)`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 2. Reputation score
|
|
147
|
+
try {
|
|
148
|
+
const reputation = agentReputation.getReputation(projectRoot, agent.name);
|
|
149
|
+
score += reputation.score / 10; // Normalize to ~0-100 range
|
|
150
|
+
if (reputation.score > 0) {
|
|
151
|
+
reasons.push(`reputation ${reputation.score}`);
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// No reputation data — neutral
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 3. Workload penalty (fewer in-progress tasks = better)
|
|
158
|
+
const workload = getAgentWorkload(projectRoot, agent.name);
|
|
159
|
+
score -= workload * 20;
|
|
160
|
+
if (workload > 0) {
|
|
161
|
+
reasons.push(`workload ${workload} tasks`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
agent: agent.name,
|
|
166
|
+
score,
|
|
167
|
+
reason: reasons.length > 0 ? reasons.join(', ') : 'general availability',
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Sort by score descending
|
|
172
|
+
scored.sort((a, b) => b.score - a.score);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
agent: scored[0].agent,
|
|
176
|
+
reason: scored[0].reason,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Generates a sprint plan from open tasks.
|
|
182
|
+
* This is an advisory suggestion — never auto-executed.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} projectRoot - Root directory
|
|
185
|
+
* @param {object} [options] - Sprint options
|
|
186
|
+
* @param {string} [options.name] - Sprint name
|
|
187
|
+
* @param {number} [options.maxTasks] - Max tasks to include
|
|
188
|
+
* @returns {SprintPlan}
|
|
189
|
+
*/
|
|
190
|
+
function generateSprintPlan(projectRoot, options = {}) {
|
|
191
|
+
const sprintName = options.name || `Sprint-${new Date().toISOString().slice(0, 10)}`;
|
|
192
|
+
const maxTasks = options.maxTasks || MAX_SPRINT_SIZE;
|
|
193
|
+
|
|
194
|
+
// Get all open/blocked tasks, prioritized
|
|
195
|
+
let tasks = [];
|
|
196
|
+
try {
|
|
197
|
+
const openTasks = taskModel.listTasks(projectRoot, { status: 'open' });
|
|
198
|
+
const blockedTasks = taskModel.listTasks(projectRoot, { status: 'blocked' });
|
|
199
|
+
tasks = [...openTasks, ...blockedTasks];
|
|
200
|
+
} catch {
|
|
201
|
+
tasks = [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Priority sort using shared utility
|
|
205
|
+
const sortedTasks = taskModel.sortByPriority(tasks);
|
|
206
|
+
|
|
207
|
+
// Take top N
|
|
208
|
+
const sprintTasks = sortedTasks.slice(0, maxTasks);
|
|
209
|
+
|
|
210
|
+
// Auto-assign each
|
|
211
|
+
/** @type {TaskAssignment[]} */
|
|
212
|
+
const assignments = sprintTasks.map((task) => {
|
|
213
|
+
const best = findBestAgent(projectRoot, task.title, task.priority);
|
|
214
|
+
return {
|
|
215
|
+
taskId: task.id,
|
|
216
|
+
taskTitle: task.title,
|
|
217
|
+
suggestedAgent: best.agent,
|
|
218
|
+
reason: best.reason,
|
|
219
|
+
priority: task.priority,
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
/** @type {SprintPlan} */
|
|
224
|
+
const plan = {
|
|
225
|
+
id: `SPR-${crypto.randomUUID().slice(0, 8).toUpperCase()}`,
|
|
226
|
+
name: sprintName,
|
|
227
|
+
createdAt: new Date().toISOString(),
|
|
228
|
+
assignments,
|
|
229
|
+
status: 'draft',
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Persist
|
|
233
|
+
const data = loadSprintData(projectRoot);
|
|
234
|
+
data.plans.push(plan);
|
|
235
|
+
writeSprintData(projectRoot, data);
|
|
236
|
+
|
|
237
|
+
return plan;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Auto-assigns a single task to the best available agent.
|
|
242
|
+
*
|
|
243
|
+
* @param {string} projectRoot - Root directory
|
|
244
|
+
* @param {string} taskId - Task ID
|
|
245
|
+
* @returns {{ success: boolean, agent?: string, reason?: string, error?: string }}
|
|
246
|
+
*/
|
|
247
|
+
function autoAssignTask(projectRoot, taskId) {
|
|
248
|
+
const task = taskModel.getTask(projectRoot, taskId);
|
|
249
|
+
if (!task) {
|
|
250
|
+
return { success: false, error: `Task not found: ${taskId}` };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const best = findBestAgent(projectRoot, task.title, task.priority);
|
|
254
|
+
|
|
255
|
+
if (best.agent === 'unassigned') {
|
|
256
|
+
return { success: false, error: best.reason };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const result = taskModel.updateTask(projectRoot, taskId, { assignee: best.agent });
|
|
260
|
+
|
|
261
|
+
if (result.success) {
|
|
262
|
+
return { success: true, agent: best.agent, reason: best.reason };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return { success: false, error: 'Failed to update task' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Suggests the next highest-priority unblocked task.
|
|
270
|
+
*
|
|
271
|
+
* @param {string} projectRoot - Root directory
|
|
272
|
+
* @returns {{ task: object | null, reason: string }}
|
|
273
|
+
*/
|
|
274
|
+
function suggestNextTask(projectRoot) {
|
|
275
|
+
let openTasks = [];
|
|
276
|
+
try {
|
|
277
|
+
openTasks = taskModel.listTasks(projectRoot, { status: 'open' });
|
|
278
|
+
} catch {
|
|
279
|
+
return { task: null, reason: 'No tasks available' };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (openTasks.length === 0) {
|
|
283
|
+
return { task: null, reason: 'No open tasks remaining' };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Priority sort using shared utility
|
|
287
|
+
const sorted = taskModel.sortByPriority(openTasks);
|
|
288
|
+
|
|
289
|
+
const topTask = sorted[0];
|
|
290
|
+
return {
|
|
291
|
+
task: topTask,
|
|
292
|
+
reason: `Highest priority open task (${topTask.priority})`,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Returns sprint velocity and progress metrics.
|
|
298
|
+
*
|
|
299
|
+
* @param {string} projectRoot - Root directory
|
|
300
|
+
* @returns {{ totalSprints: number, activeSprint: SprintPlan | null, velocity: number, completionRate: number }}
|
|
301
|
+
*/
|
|
302
|
+
function getSprintMetrics(projectRoot) {
|
|
303
|
+
const data = loadSprintData(projectRoot);
|
|
304
|
+
const activeSprint = data.plans.find((p) => p.status === 'active') || null;
|
|
305
|
+
const completedSprints = data.plans.filter((p) => p.status === 'completed');
|
|
306
|
+
|
|
307
|
+
// Velocity: average assignments per completed sprint
|
|
308
|
+
const velocity = completedSprints.length > 0
|
|
309
|
+
? Math.round(
|
|
310
|
+
completedSprints.reduce((sum, s) => sum + s.assignments.length, 0) / completedSprints.length
|
|
311
|
+
)
|
|
312
|
+
: 0;
|
|
313
|
+
|
|
314
|
+
// Task metrics
|
|
315
|
+
let completionRate = 0;
|
|
316
|
+
try {
|
|
317
|
+
const metrics = taskModel.getTaskMetrics(projectRoot);
|
|
318
|
+
completionRate = metrics.completionRate;
|
|
319
|
+
} catch {
|
|
320
|
+
completionRate = 0;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
totalSprints: data.plans.length,
|
|
325
|
+
activeSprint,
|
|
326
|
+
velocity,
|
|
327
|
+
completionRate,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
module.exports = {
|
|
332
|
+
generateSprintPlan,
|
|
333
|
+
autoAssignTask,
|
|
334
|
+
suggestNextTask,
|
|
335
|
+
getSprintMetrics,
|
|
336
|
+
};
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devran AI Kit — Error Budget Tracker
|
|
3
|
+
*
|
|
4
|
+
* Enables reliability-config.json error budget tracking with
|
|
5
|
+
* basic metrics collection. Records test, build, and deployment
|
|
6
|
+
* results and calculates budget health.
|
|
7
|
+
*
|
|
8
|
+
* @module lib/error-budget
|
|
9
|
+
* @author Emre Dursun
|
|
10
|
+
* @since v3.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const { AGENT_DIR, ENGINE_DIR } = require('./constants');
|
|
19
|
+
const { writeJsonAtomic } = require('./io');
|
|
20
|
+
const { createLogger } = require('./logger');
|
|
21
|
+
const log = createLogger('error-budget');
|
|
22
|
+
const RELIABILITY_CONFIG = 'reliability-config.json';
|
|
23
|
+
const METRICS_FILE = 'metrics.json';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {'HEALTHY' | 'WARNING' | 'EXHAUSTED'} BudgetStatus
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} MetricsData
|
|
31
|
+
* @property {number} testsPassed - Total tests passed
|
|
32
|
+
* @property {number} testsFailed - Total tests failed
|
|
33
|
+
* @property {number} buildsSucceeded - Total successful builds
|
|
34
|
+
* @property {number} buildsFailed - Total failed builds
|
|
35
|
+
* @property {number} deploysSucceeded - Total successful deploys
|
|
36
|
+
* @property {number} deploysRolledBack - Total rolled-back deploys
|
|
37
|
+
* @property {string} periodStart - ISO timestamp of period start
|
|
38
|
+
* @property {string} lastUpdated - ISO timestamp of last update
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {object} BudgetReport
|
|
43
|
+
* @property {BudgetStatus} status - Overall budget health
|
|
44
|
+
* @property {object} rates - Current failure rates
|
|
45
|
+
* @property {number} rates.testFailureRate - Test failure rate (%)
|
|
46
|
+
* @property {number} rates.buildFailureRate - Build failure rate (%)
|
|
47
|
+
* @property {number} rates.deployRollbackRate - Deploy rollback rate (%)
|
|
48
|
+
* @property {object} thresholds - Configured thresholds
|
|
49
|
+
* @property {number} thresholds.testFailureRatePercent - Max test failure rate
|
|
50
|
+
* @property {number} thresholds.buildFailureRatePercent - Max build failure rate
|
|
51
|
+
* @property {number} thresholds.deployRollbackRatePercent - Max deploy rollback rate
|
|
52
|
+
* @property {string[]} violations - Which metrics exceeded thresholds
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolves the path to the metrics file.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} projectRoot - Root directory of the project
|
|
59
|
+
* @returns {string} Absolute path to metrics.json
|
|
60
|
+
*/
|
|
61
|
+
function resolveMetricsPath(projectRoot) {
|
|
62
|
+
return path.join(projectRoot, AGENT_DIR, ENGINE_DIR, METRICS_FILE);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolves the path to the reliability config.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} projectRoot - Root directory of the project
|
|
69
|
+
* @returns {string} Absolute path to reliability-config.json
|
|
70
|
+
*/
|
|
71
|
+
function resolveConfigPath(projectRoot) {
|
|
72
|
+
return path.join(projectRoot, AGENT_DIR, ENGINE_DIR, RELIABILITY_CONFIG);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Loads the reliability configuration.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} projectRoot - Root directory of the project
|
|
79
|
+
* @returns {object} Parsed reliability config
|
|
80
|
+
*/
|
|
81
|
+
function loadConfig(projectRoot) {
|
|
82
|
+
const configPath = resolveConfigPath(projectRoot);
|
|
83
|
+
|
|
84
|
+
if (!fs.existsSync(configPath)) {
|
|
85
|
+
throw new Error(`Reliability config not found: ${configPath}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Loads metrics data from disk, or returns defaults if file doesn't exist.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} projectRoot - Root directory of the project
|
|
95
|
+
* @returns {MetricsData}
|
|
96
|
+
*/
|
|
97
|
+
function loadMetrics(projectRoot) {
|
|
98
|
+
const metricsPath = resolveMetricsPath(projectRoot);
|
|
99
|
+
|
|
100
|
+
if (!fs.existsSync(metricsPath)) {
|
|
101
|
+
return createEmptyMetrics();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(fs.readFileSync(metricsPath, 'utf-8'));
|
|
106
|
+
} catch {
|
|
107
|
+
return createEmptyMetrics();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Creates an empty metrics object.
|
|
113
|
+
*
|
|
114
|
+
* @returns {MetricsData}
|
|
115
|
+
*/
|
|
116
|
+
function createEmptyMetrics() {
|
|
117
|
+
return {
|
|
118
|
+
testsPassed: 0,
|
|
119
|
+
testsFailed: 0,
|
|
120
|
+
buildsSucceeded: 0,
|
|
121
|
+
buildsFailed: 0,
|
|
122
|
+
deploysSucceeded: 0,
|
|
123
|
+
deploysRolledBack: 0,
|
|
124
|
+
periodStart: new Date().toISOString(),
|
|
125
|
+
lastUpdated: new Date().toISOString(),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Writes metrics data to disk atomically.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} projectRoot - Root directory of the project
|
|
133
|
+
* @param {MetricsData} metrics - Metrics data to write
|
|
134
|
+
* @returns {void}
|
|
135
|
+
*/
|
|
136
|
+
function writeMetrics(projectRoot, metrics) {
|
|
137
|
+
const metricsPath = resolveMetricsPath(projectRoot);
|
|
138
|
+
|
|
139
|
+
const updatedMetrics = {
|
|
140
|
+
...metrics,
|
|
141
|
+
lastUpdated: new Date().toISOString(),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
writeJsonAtomic(metricsPath, updatedMetrics);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Records the result of a test run.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} projectRoot - Root directory of the project
|
|
151
|
+
* @param {number} passed - Number of tests passed
|
|
152
|
+
* @param {number} failed - Number of tests failed
|
|
153
|
+
* @returns {void}
|
|
154
|
+
*/
|
|
155
|
+
function recordTestResult(projectRoot, passed, failed) {
|
|
156
|
+
const metrics = loadMetrics(projectRoot);
|
|
157
|
+
writeMetrics(projectRoot, {
|
|
158
|
+
...metrics,
|
|
159
|
+
testsPassed: metrics.testsPassed + passed,
|
|
160
|
+
testsFailed: metrics.testsFailed + failed,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Records the result of a build.
|
|
166
|
+
*
|
|
167
|
+
* @param {string} projectRoot - Root directory of the project
|
|
168
|
+
* @param {boolean} success - Whether the build succeeded
|
|
169
|
+
* @returns {void}
|
|
170
|
+
*/
|
|
171
|
+
function recordBuildResult(projectRoot, success) {
|
|
172
|
+
const metrics = loadMetrics(projectRoot);
|
|
173
|
+
writeMetrics(projectRoot, {
|
|
174
|
+
...metrics,
|
|
175
|
+
buildsSucceeded: metrics.buildsSucceeded + (success ? 1 : 0),
|
|
176
|
+
buildsFailed: metrics.buildsFailed + (success ? 0 : 1),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Records the result of a deployment.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} projectRoot - Root directory of the project
|
|
184
|
+
* @param {boolean} success - Whether deploy succeeded
|
|
185
|
+
* @param {boolean} [rolledBack=false] - Whether it was rolled back
|
|
186
|
+
* @returns {void}
|
|
187
|
+
*/
|
|
188
|
+
function recordDeployResult(projectRoot, success, rolledBack = false) {
|
|
189
|
+
const metrics = loadMetrics(projectRoot);
|
|
190
|
+
writeMetrics(projectRoot, {
|
|
191
|
+
...metrics,
|
|
192
|
+
deploysSucceeded: metrics.deploysSucceeded + (success && !rolledBack ? 1 : 0),
|
|
193
|
+
deploysRolledBack: metrics.deploysRolledBack + (rolledBack ? 1 : 0),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Calculates a failure rate percentage safely.
|
|
199
|
+
*
|
|
200
|
+
* @param {number} failed - Number of failures
|
|
201
|
+
* @param {number} total - Total attempts
|
|
202
|
+
* @returns {number} Failure rate as percentage (0-100)
|
|
203
|
+
*/
|
|
204
|
+
function calculateRate(failed, total) {
|
|
205
|
+
if (total === 0) {
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
return Number(((failed / total) * 100).toFixed(2));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Generates a budget health report.
|
|
213
|
+
*
|
|
214
|
+
* @param {string} projectRoot - Root directory of the project
|
|
215
|
+
* @returns {BudgetReport}
|
|
216
|
+
*/
|
|
217
|
+
function getBudgetReport(projectRoot) {
|
|
218
|
+
const config = loadConfig(projectRoot);
|
|
219
|
+
const metrics = loadMetrics(projectRoot);
|
|
220
|
+
const thresholds = config.errorBudget?.thresholds || {};
|
|
221
|
+
|
|
222
|
+
const testTotal = metrics.testsPassed + metrics.testsFailed;
|
|
223
|
+
const buildTotal = metrics.buildsSucceeded + metrics.buildsFailed;
|
|
224
|
+
const deployTotal = metrics.deploysSucceeded + metrics.deploysRolledBack;
|
|
225
|
+
|
|
226
|
+
const rates = {
|
|
227
|
+
testFailureRate: calculateRate(metrics.testsFailed, testTotal),
|
|
228
|
+
buildFailureRate: calculateRate(metrics.buildsFailed, buildTotal),
|
|
229
|
+
deployRollbackRate: calculateRate(metrics.deploysRolledBack, deployTotal),
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/** @type {string[]} */
|
|
233
|
+
const violations = [];
|
|
234
|
+
|
|
235
|
+
if (rates.testFailureRate > (thresholds.testFailureRatePercent || 5)) {
|
|
236
|
+
violations.push('testFailureRate');
|
|
237
|
+
}
|
|
238
|
+
if (rates.buildFailureRate > (thresholds.buildFailureRatePercent || 2)) {
|
|
239
|
+
violations.push('buildFailureRate');
|
|
240
|
+
}
|
|
241
|
+
if (rates.deployRollbackRate > (thresholds.deployRollbackRatePercent || 10)) {
|
|
242
|
+
violations.push('deployRollbackRate');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** @type {BudgetStatus} */
|
|
246
|
+
let status = 'HEALTHY';
|
|
247
|
+
|
|
248
|
+
if (violations.length > 0) {
|
|
249
|
+
status = 'EXHAUSTED';
|
|
250
|
+
} else {
|
|
251
|
+
// Warning if any rate is above 80% of threshold
|
|
252
|
+
const testWarning = rates.testFailureRate > (thresholds.testFailureRatePercent || 5) * 0.8;
|
|
253
|
+
const buildWarning = rates.buildFailureRate > (thresholds.buildFailureRatePercent || 2) * 0.8;
|
|
254
|
+
const deployWarning = rates.deployRollbackRate > (thresholds.deployRollbackRatePercent || 10) * 0.8;
|
|
255
|
+
|
|
256
|
+
if (testWarning || buildWarning || deployWarning) {
|
|
257
|
+
status = 'WARNING';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
status,
|
|
263
|
+
rates,
|
|
264
|
+
thresholds: {
|
|
265
|
+
testFailureRatePercent: thresholds.testFailureRatePercent || 5,
|
|
266
|
+
buildFailureRatePercent: thresholds.buildFailureRatePercent || 2,
|
|
267
|
+
deployRollbackRatePercent: thresholds.deployRollbackRatePercent || 10,
|
|
268
|
+
},
|
|
269
|
+
violations,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Archives current metrics before resetting.
|
|
275
|
+
* Preserves historical data for trend analysis.
|
|
276
|
+
*
|
|
277
|
+
* @param {string} projectRoot - Root directory of the project
|
|
278
|
+
* @returns {{ archived: boolean, archivePath: string | null }}
|
|
279
|
+
*/
|
|
280
|
+
function archiveMetrics(projectRoot) {
|
|
281
|
+
const metrics = loadMetrics(projectRoot);
|
|
282
|
+
const hasData = metrics.testsPassed + metrics.testsFailed +
|
|
283
|
+
metrics.buildsSucceeded + metrics.buildsFailed +
|
|
284
|
+
metrics.deploysSucceeded + metrics.deploysRolledBack > 0;
|
|
285
|
+
|
|
286
|
+
if (!hasData) {
|
|
287
|
+
return { archived: false, archivePath: null };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const archiveDir = path.join(projectRoot, AGENT_DIR, ENGINE_DIR, 'metrics-archive');
|
|
291
|
+
if (!fs.existsSync(archiveDir)) {
|
|
292
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
296
|
+
const archivePath = path.join(archiveDir, `metrics-${timestamp}.json`);
|
|
297
|
+
|
|
298
|
+
const report = getBudgetReport(projectRoot);
|
|
299
|
+
const archiveEntry = {
|
|
300
|
+
...metrics,
|
|
301
|
+
archivedAt: new Date().toISOString(),
|
|
302
|
+
budgetStatus: report.status,
|
|
303
|
+
rates: report.rates,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
fs.writeFileSync(archivePath, JSON.stringify(archiveEntry, null, 2) + '\n', 'utf-8');
|
|
307
|
+
|
|
308
|
+
return { archived: true, archivePath };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Resets metrics for a new tracking period.
|
|
313
|
+
* Automatically archives previous period's data before resetting.
|
|
314
|
+
*
|
|
315
|
+
* @param {string} projectRoot - Root directory of the project
|
|
316
|
+
* @param {object} [options] - Reset options
|
|
317
|
+
* @param {boolean} [options.skipArchive=false] - Skip archival
|
|
318
|
+
* @returns {{ archived: boolean, archivePath: string | null }}
|
|
319
|
+
*/
|
|
320
|
+
function resetMetrics(projectRoot, options = {}) {
|
|
321
|
+
let archiveResult = { archived: false, archivePath: null };
|
|
322
|
+
|
|
323
|
+
if (!options.skipArchive) {
|
|
324
|
+
archiveResult = archiveMetrics(projectRoot);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
writeMetrics(projectRoot, createEmptyMetrics());
|
|
328
|
+
return archiveResult;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Returns historical metrics from the archive.
|
|
333
|
+
*
|
|
334
|
+
* @param {string} projectRoot - Root directory of the project
|
|
335
|
+
* @param {number} [limit=10] - Maximum entries to return
|
|
336
|
+
* @returns {object[]}
|
|
337
|
+
*/
|
|
338
|
+
function getMetricsHistory(projectRoot, limit = 10) {
|
|
339
|
+
const archiveDir = path.join(projectRoot, AGENT_DIR, ENGINE_DIR, 'metrics-archive');
|
|
340
|
+
|
|
341
|
+
if (!fs.existsSync(archiveDir)) {
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const files = fs.readdirSync(archiveDir)
|
|
346
|
+
.filter((f) => f.startsWith('metrics-') && f.endsWith('.json'))
|
|
347
|
+
.sort()
|
|
348
|
+
.reverse()
|
|
349
|
+
.slice(0, limit);
|
|
350
|
+
|
|
351
|
+
return files.map((file) => {
|
|
352
|
+
try {
|
|
353
|
+
return JSON.parse(fs.readFileSync(path.join(archiveDir, file), 'utf-8'));
|
|
354
|
+
} catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}).filter(Boolean);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
module.exports = {
|
|
361
|
+
loadConfig,
|
|
362
|
+
loadMetrics,
|
|
363
|
+
recordTestResult,
|
|
364
|
+
recordBuildResult,
|
|
365
|
+
recordDeployResult,
|
|
366
|
+
getBudgetReport,
|
|
367
|
+
resetMetrics,
|
|
368
|
+
archiveMetrics,
|
|
369
|
+
getMetricsHistory,
|
|
370
|
+
};
|