UNPKG

arela

Version:

AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.

183 lines 6.44 kB
import path from "path"; import fs from "fs-extra"; import { glob } from "glob"; const COMPLEX_KEYWORDS = [ /security/i, /workflow/i, /architecture/i, /observability/i, /compliance/i, /evaluation/i, ]; const SIMPLE_KEYWORDS = [ /missing test/i, /console\.log/i, /lint/i, /format/i, /typo/i, /spelling/i, ]; export async function generateTicketsFromViolations(opts) { if (!opts.violations.length) { return []; } const groups = buildTicketGroups(opts.violations); // Prioritize larger groups first for deterministic IDs groups.sort((a, b) => b.violations.length - a.violations.length); const tickets = []; for (const group of groups) { const id = await getNextTicketId(opts.cwd, group.agent, { ensureDir: !opts.dryRun }); const ticketPath = path.join(opts.cwd, ".arela", "tickets", `${id}.md`); const content = renderTicket(id, group, opts.cwd); if (!opts.dryRun) { await fs.ensureDir(path.dirname(ticketPath)); await fs.writeFile(ticketPath, content, "utf8"); } const files = buildFileList(group.violations); tickets.push({ id, title: group.title, summary: group.summary, agent: group.agent, priority: group.priority, complexity: group.complexity, estimate: group.estimate, occurrences: group.violations.length, files, path: ticketPath, dryRun: Boolean(opts.dryRun), }); } return tickets; } function buildTicketGroups(violations) { const groups = new Map(); for (const violation of violations) { const key = getGroupKey(violation); const existing = groups.get(key); if (existing) { existing.violations.push(violation); continue; } const { agent, complexity, priority, estimate } = classifyViolation(violation); const { title, summary } = describeViolation(violation); groups.set(key, { key, title, summary, agent, priority, complexity, estimate, violations: [violation], }); } return Array.from(groups.values()); } function getGroupKey(violation) { const basis = violation.ruleId?.toLowerCase() ?? normalizeText(violation.message); return `${violation.source || "general"}:${basis}`; } function normalizeText(value) { return value .toLowerCase() .replace(/\d+/g, "{n}") .replace(/\s+/g, " ") .trim(); } function classifyViolation(violation) { const text = `${violation.message} ${violation.ruleId ?? ""}`; if (COMPLEX_KEYWORDS.some((pattern) => pattern.test(text))) { return { agent: "claude", complexity: "complex", priority: "high", estimate: "1h" }; } if (SIMPLE_KEYWORDS.some((pattern) => pattern.test(text))) { return { agent: "codex", complexity: "simple", priority: "medium", estimate: "30m" }; } return { agent: "codex", complexity: "medium", priority: "medium", estimate: "45m" }; } function describeViolation(violation) { const base = violation.message.split(/\r?\n/)[0]?.trim() || "Resolve violation"; const clean = base.replace(/^Error\s*:/i, "").trim(); const summary = clean.charAt(0).toUpperCase() + clean.slice(1); const title = violation.ruleId ? `Resolve ${violation.ruleId}` : summary; return { title, summary }; } async function getNextTicketId(cwd, agent, opts) { const ticketsDir = path.join(cwd, ".arela", "tickets"); const shouldEnsure = opts?.ensureDir ?? true; const dirExists = await fs.pathExists(ticketsDir); const files = dirExists ? await glob("**/*.md", { cwd: ticketsDir, nodir: true }) : []; const prefix = agent.toUpperCase(); const pattern = new RegExp(`^${prefix}-(\\d+)\\.md$`); let max = 0; for (const file of files) { const match = file.match(pattern); if (match) { const value = Number(match[1]); if (!Number.isNaN(value)) { max = Math.max(max, value); } } } if (!dirExists && shouldEnsure) { await fs.ensureDir(ticketsDir); } const next = String(max + 1).padStart(3, "0"); return `${prefix}-${next}`; } function renderTicket(id, group, cwd) { const violationList = group.violations .map((violation, index) => formatViolationLine(index + 1, violation, cwd)) .join("\n"); const files = buildFileList(group.violations); const fileLine = files.length ? files.join(", ") : "(file not reported)"; return [ `# ${id}: ${group.title}`, "", `**Complexity:** ${group.complexity}`, `**Priority:** ${group.priority}`, `**Agent:** ${group.agent}`, `**Estimated time:** ${group.estimate}`, "", "## Summary", "", `${group.summary} (${group.violations.length} occurrence${group.violations.length === 1 ? "" : "s"}).`, "", "## Impact", "", `- Files: ${fileLine}`, `- Source: ${group.violations[0]?.source ?? "violation"}`, "", "## Violations", "", violationList || "- No detailed information provided", "", "## Acceptance Criteria", "", "- [ ] All listed violations are resolved", "- [ ] Relevant tests cover the regression", "", ].join("\n"); } function formatViolationLine(index, violation, cwd) { const relPath = violation.file ? normalizePath(violation.file, cwd) : "(unknown file)"; const location = violation.line ? `${relPath}:${violation.line}` : relPath; const detail = violation.message; return `${index}. \`${location}\` - ${detail}`; } function normalizePath(target, cwd) { const normalized = path.isAbsolute(target) ? path.relative(cwd, target) : target; return normalized.replace(/\\/g, "/"); } function buildFileList(violations) { const files = new Set(); for (const violation of violations) { if (!violation.file) { continue; } const withLine = violation.line ? `${violation.file}:${violation.line}` : violation.file; files.add(withLine.replace(/\\/g, "/")); } return Array.from(files); } //# sourceMappingURL=auto-generate.js.map