import * as fs from "node:fs"; import * as path from "node:path"; import { execSync } from "node:child_process"; import { CIAgentFiles } from "./ciagent-files.js"; import { GitContext } from "./git-context.js"; export type IdeationSource = | "uncovered_requirement" | "repeated_lesson" | "low_confidence_decision" | "escalation_pattern" | "compound_pattern" | "partial_requirement" | "gap_in_coverage" | "improvement_pattern" | "architecture_drift" | "verification_inversion" | "spec_ambiguity" | "spec_contradiction" | "spec_missing" | "external_signal" | "cross_project_lesson" | "chaos_scenario"; export type IdeationCategory = | "security" | "quality" | "architecture" | "coverage" | "improvement" | "spec" | "chaos"; export type IdeationAction = | "add_requirement" | "update_architecture" | "update_roadmap" | "fix_documentation" | "add_test" | "add_security_pattern" | "refactor" | "new_milestone_phase"; export type IdeationTier = "mechanical" | "backend-enriched" | "cross-project"; export interface Idea { id: string; source: IdeationSource; category: IdeationCategory; title: string; rationale: string; confidence: number; relatedReq?: string; actions: IdeationAction[]; tier: IdeationTier; } export interface IdeationResult { project: string; milestone: string; ideas: Idea[]; summary: IdeationSummary; } export interface IdeationSummary { total: number; accepted: number; skipped: number; by_category: Record; by_tier: Record; } export interface IdeationConfig { enabled: boolean; categories: IdeationCategory[]; confidence_threshold: number; max_ideas: number; external_signals: { npm_audit: boolean; osv_advisories: boolean; dependency_staleness: boolean; }; cross_project: { enabled: boolean; similarity_weight: number; }; chaos: { enabled: boolean; scenarios: string[]; }; } export const DEFAULT_IDEATION_CONFIG: IdeationConfig = { enabled: true, categories: ["security", "quality", "architecture", "coverage", "improvement"], confidence_threshold: 0.6, max_ideas: 20, external_signals: { npm_audit: true, osv_advisories: true, dependency_staleness: true, }, cross_project: { enabled: false, similarity_weight: 0.5, }, chaos: { enabled: true, scenarios: ["backend_unavailable", "requirement_change", "test_coverage_drop"], }, }; let ideaCounter = 0; function nextIdeaId(): string { ideaCounter++; return `IDEATE-${String(ideaCounter).padStart(2, "0")}`; } export function resetIdeaCounter(): void { ideaCounter = 0; } export class IdeationEngine { private ciFiles: CIAgentFiles; private projectPath: string; constructor(projectPath: string, projectSlug?: string) { this.projectPath = projectPath; this.ciFiles = new CIAgentFiles(projectPath); if (projectSlug) { this.ciFiles.setProjectSlug(projectSlug); } } runMechanical(categories?: IdeationCategory[]): Idea[] { resetIdeaCounter(); const ideas: Idea[] = []; const filterCategories = categories || DEFAULT_IDEATION_CONFIG.categories; const shouldCategory = (cat: IdeationCategory): boolean => filterCategories.length === 0 || filterCategories.includes(cat); if (shouldCategory("coverage")) { ideas.push(...this.mineUncoveredRequirements()); ideas.push(...this.minePartialRequirements()); ideas.push(...this.mineCoverageGaps()); } if (shouldCategory("quality") || shouldCategory("improvement")) { ideas.push(...this.mineRepeatedLessons()); ideas.push(...this.mineLowConfidenceDecisions()); ideas.push(...this.mineCompoundPatterns()); } if (shouldCategory("architecture")) { ideas.push(...this.mineArchitectureDrift()); } if (shouldCategory("security")) { ideas.push(...this.mineEscalationPatterns()); } if (shouldCategory("improvement")) { ideas.push(...this.mineImprovementPatterns()); } if (shouldCategory("spec")) { ideas.push(...this.mineSpecAmbiguity()); ideas.push(...this.mineSpecContradictions()); ideas.push(...this.mineSpecMissing()); } if (shouldCategory("quality")) { ideas.push(...this.mineVerificationInversion()); } ideas.sort((a, b) => b.confidence - a.confidence); return ideas.slice(0, DEFAULT_IDEATION_CONFIG.max_ideas); } private mineUncoveredRequirements(): Idea[] { const ideas: Idea[] = []; const reqs = this.ciFiles.readRequirementsMd(); if (!reqs) return ideas; const coveredReqs = new Set(); for (const t of reqs.traceability) { if (t.status === "complete") { coveredReqs.add(t.requirement); } } const allReqIds = new Set(); for (const cat of [...reqs.v1, ...reqs.v2]) { for (const item of cat.items) { allReqIds.add(item.id); } } for (const reqId of allReqIds) { if (!coveredReqs.has(reqId)) { ideas.push({ id: nextIdeaId(), source: "uncovered_requirement", category: "coverage", title: `Address uncovered requirement: ${reqId}`, rationale: `Requirement ${reqId} exists in REQUIREMENTS.md but has no completed implementation traceability record.`, confidence: 0.85, relatedReq: reqId, actions: ["add_requirement", "update_roadmap"], tier: "mechanical", }); } } return ideas; } private minePartialRequirements(): Idea[] { const ideas: Idea[] = []; const reqs = this.ciFiles.readRequirementsMd(); if (!reqs) return ideas; for (const t of reqs.traceability) { if (t.status === "in_progress") { ideas.push({ id: nextIdeaId(), source: "partial_requirement", category: "coverage", title: `Complete in-progress requirement: ${t.requirement}`, rationale: `Requirement ${t.requirement} (Phase ${t.phase}) is in progress but not complete. In-progress items may be blocked or abandoned.`, confidence: 0.75, relatedReq: t.requirement, actions: ["add_requirement"], tier: "mechanical", }); } } return ideas; } private mineCoverageGaps(): Idea[] { const ideas: Idea[] = []; const projectMd = this.ciFiles.readProjectMd(); if (!projectMd) return ideas; const mentionedAgents: string[] = []; const agentRegex = /(?:agent|Agent)[:\s]+(\S+)/g; let match; while ((match = agentRegex.exec(projectMd.coreValue || "")) !== null) { mentionedAgents.push(match[1]); } const agentsDir = path.join(this.projectPath, "src", "agents"); if (!fs.existsSync(agentsDir)) return ideas; const existingAgents = new Set( fs.readdirSync(agentsDir) .filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".d.ts") && f !== "index.ts" && f !== "base.ts") .map((f) => f.replace(".ts", "")) ); for (const agent of mentionedAgents) { if (!existingAgents.has(agent) && !existingAgents.has(agent.replace(/-agent$/, ""))) { ideas.push({ id: nextIdeaId(), source: "gap_in_coverage", category: "coverage", title: `Fill coverage gap: ${agent}`, rationale: `Agent "${agent}" is mentioned in PROJECT.md but not found in the agent registry.`, confidence: 0.75, actions: ["add_requirement"], tier: "mechanical", }); } } return ideas; } private mineRepeatedLessons(): Idea[] { const ideas: Idea[] = []; const lessons = this.readGitLessons(); const topicCounts: Record = {}; const topicDetails: Record = {}; for (const lesson of lessons) { const topic = lesson.topic; topicCounts[topic] = (topicCounts[topic] || 0) + 1; if (!topicDetails[topic]) topicDetails[topic] = []; topicDetails[topic].push(lesson.detail); } for (const [topic, count] of Object.entries(topicCounts)) { if (count > 1) { ideas.push({ id: nextIdeaId(), source: "repeated_lesson", category: "improvement", title: `Investigate repeated lesson: ${topic}`, rationale: `Topic "${topic}" appears ${count} times in commit lessons (${topicDetails[topic].slice(0, 2).join("; ")}), indicating a systemic issue.`, confidence: Math.min(0.7 + count * 0.05, 0.95), actions: ["add_requirement", "refactor"], tier: "mechanical", }); } } return ideas; } private mineLowConfidenceDecisions(): Idea[] { const ideas: Idea[] = []; try { const log = execSync( 'git log --all --grep="decisions:" --format="%B" -50', { cwd: this.projectPath, encoding: "utf-8", timeout: 5000 } ); const decisionRegex = /confidence:\s*([\d.]+)/gi; let match; while ((match = decisionRegex.exec(log)) !== null) { const confidence = parseFloat(match[1]); if (confidence < 0.7 && confidence > 0) { const contextStart = Math.max(0, match.index - 200); const context = log.slice(contextStart, match.index + 100); const idMatch = context.match(/id:\s*(D-\d+)/i); ideas.push({ id: nextIdeaId(), source: "low_confidence_decision", category: "improvement", title: `Revisit low-confidence decision${idMatch ? ` ${idMatch[1]}` : ""}`, rationale: `A decision was made with confidence ${confidence.toFixed(2)} (below 0.7 threshold). Low-confidence decisions are prime candidates for re-evaluation.`, confidence: 0.8, actions: ["update_roadmap"], tier: "mechanical", }); } } } catch {} return ideas; } private mineEscalationPatterns(): Idea[] { const ideas: Idea[] = []; try { const log = execSync( 'git log --all --grep="escalation:" --format="%B" -50', { cwd: this.projectPath, encoding: "utf-8", timeout: 5000 } ); const typeCounts: Record = {}; const escalationRegex = /type:\s*(\S+)/gi; let match; while ((match = escalationRegex.exec(log)) !== null) { const type = match[1].toLowerCase(); typeCounts[type] = (typeCounts[type] || 0) + 1; } for (const [type, count] of Object.entries(typeCounts)) { if (count >= 1) { ideas.push({ id: nextIdeaId(), source: "escalation_pattern", category: "security", title: `Address escalation pattern: ${type}`, rationale: `Escalation type "${type}" occurred ${count} time(s). Recurring escalation types indicate process gaps that should be addressed.`, confidence: 0.7 + Math.min(count * 0.1, 0.2), actions: ["add_security_pattern", "update_roadmap"], tier: "mechanical", }); } } } catch {} return ideas; } private mineCompoundPatterns(): Idea[] { const ideas: Idea[] = []; try { const log = execSync( 'git log --all --grep="compound:" --format="%B" -50', { cwd: this.projectPath, encoding: "utf-8", timeout: 5000 } ); const compoundRegex = /compound:\s*\n((?:\s+-\s+.+\n?)+)/g; const topicCounts: Record = {}; let match; while ((match = compoundRegex.exec(log)) !== null) { const items = match[1].split("\n").filter((l: string) => l.trim().startsWith("-")); for (const item of items) { const detail = item.replace(/^\s*-\s*/, "").trim(); const topic = detail.split(":")[0].trim().toLowerCase(); topicCounts[topic] = (topicCounts[topic] || 0) + 1; } } for (const [topic, count] of Object.entries(topicCounts)) { if (count > 1) { ideas.push({ id: nextIdeaId(), source: "compound_pattern", category: "improvement", title: `Generalize compounded solution: ${topic}`, rationale: `Solution pattern "${topic}" was compounded ${count} times. Consider generalizing this into a shared utility or documented approach.`, confidence: 0.75, actions: ["refactor", "update_architecture"], tier: "mechanical", }); } } } catch {} return ideas; } private mineArchitectureDrift(): Idea[] { const ideas: Idea[] = []; const archMd = this.ciFiles.readArchitectureMd(); if (!archMd) return ideas; for (const component of archMd.components) { const expectedDir = component.name.toLowerCase().replace(/\s+/g, "-"); const possiblePaths = [ path.join(this.projectPath, "src", expectedDir), path.join(this.projectPath, "src", component.name), path.join(this.projectPath, component.name.toLowerCase()), ]; const dirExists = possiblePaths.some((p) => fs.existsSync(p)); if (!dirExists) { ideas.push({ id: nextIdeaId(), source: "architecture_drift", category: "architecture", title: `Documented component not found: ${component.name}`, rationale: `ARCHITECTURE.md documents component "${component.name}" but no corresponding directory exists in src/. Either the component is missing or the documentation is stale.`, confidence: 0.7, actions: ["update_architecture", "fix_documentation"], tier: "mechanical", }); } } const srcDir = path.join(this.projectPath, "src"); if (fs.existsSync(srcDir)) { const knownComponents = new Set(archMd.components.map((c) => c.name.toLowerCase().replace(/\s+/g, "-"))); try { const srcEntries = fs.readdirSync(srcDir, { withFileTypes: true }) .filter((d) => d.isDirectory()) .map((d) => d.name.toLowerCase()); for (const entry of srcEntries) { if (!knownComponents.has(entry) && !entry.startsWith(".") && entry !== "types" && entry !== "utils") { ideas.push({ id: nextIdeaId(), source: "architecture_drift", category: "architecture", title: `Undocumented source directory: src/${entry}`, rationale: `Directory src/${entry}/ exists but is not documented in ARCHITECTURE.md. This indicates architectural drift.`, confidence: 0.65, actions: ["update_architecture"], tier: "mechanical", }); } } } catch {} } return ideas; } private mineVerificationInversion(): Idea[] { const ideas: Idea[] = []; const srcDir = path.join(this.projectPath, "src"); if (!fs.existsSync(srcDir)) return ideas; const testFiles: string[] = []; const srcFiles: string[] = []; try { const walkDir = (dir: string) => { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") { walkDir(fullPath); } else if (entry.isFile() && entry.name.endsWith(".ts")) { if (entry.name.endsWith(".test.ts")) { testFiles.push(entry.name.replace(".test.ts", "")); } else if (!entry.name.endsWith(".d.ts") && !entry.name.includes(".test.")) { srcFiles.push(entry.name.replace(".ts", "")); } } } }; walkDir(srcDir); } catch {} const testedModules = new Set(testFiles); for (const srcModule of srcFiles) { if (!testedModules.has(srcModule) && srcModule !== "index" && srcModule !== "base") { ideas.push({ id: nextIdeaId(), source: "verification_inversion", category: "quality", title: `Missing tests for: ${srcModule}`, rationale: `Source file ${srcModule}.ts has no corresponding test file ${srcModule}.test.ts. The behavioral verification layer identifies this as a coverage gap.`, confidence: 0.7, actions: ["add_test"], tier: "mechanical", }); } } return ideas.slice(0, 10); } private mineImprovementPatterns(): Idea[] { const ideas: Idea[] = []; const reqs = this.ciFiles.readRequirementsMd(); const lessons = this.readGitLessons(); if (!reqs) return ideas; const uncoveredSet = new Set(); for (const t of reqs.traceability) { if (t.status === "pending") { uncoveredSet.add(t.requirement); } } const topics = lessons.map((l) => l.topic.toLowerCase()); for (const reqId of uncoveredSet) { for (const topic of topics) { if (reqId.toLowerCase().includes(topic) || topic.includes(reqId.toLowerCase())) { ideas.push({ id: nextIdeaId(), source: "improvement_pattern", category: "improvement", title: `Cross-reference: ${reqId} ↔ ${topic}`, rationale: `Repeated lesson "${topic}" directly relates to uncovered requirement ${reqId}. Addressing the lesson may resolve the requirement.`, confidence: 0.85, relatedReq: reqId, actions: ["add_requirement", "update_roadmap"], tier: "mechanical", }); } } } return ideas; } private mineSpecAmbiguity(): Idea[] { const ideas: Idea[] = []; const projectMd = this.ciFiles.readProjectMd(); if (!projectMd) return ideas; const ambiguousTerms = ["should", "could", "might", "may", "would", "possibly", "perhaps"]; const specText = [projectMd.coreValue, ...projectMd.requirements.active].join(" "); for (const term of ambiguousTerms) { const regex = new RegExp(`\\b${term}\\b`, "gi"); const matches = specText.match(regex); if (matches && matches.length > 2) { ideas.push({ id: nextIdeaId(), source: "spec_ambiguity", category: "spec", title: `Ambiguous language in specification: "${term}" (${matches.length} occurrences)`, rationale: `The term "${term}" appears ${matches.length} times in project specification. Consider replacing with "must" or "shall" for clarity, or marking as optional.`, confidence: 0.65, actions: ["fix_documentation"], tier: "mechanical", }); } } return ideas; } private mineSpecContradictions(): Idea[] { return []; } private mineSpecMissing(): Idea[] { const ideas: Idea[] = []; const projectMd = this.ciFiles.readProjectMd(); if (!projectMd) return ideas; const specText = (projectMd.coreValue + " " + projectMd.requirements.active.join(" ")).toLowerCase(); const commonCategories: Array<{ keyword: string; title: string; category: IdeationCategory }> = [ { keyword: "auth", title: "Add authentication and authorization requirements", category: "security" }, { keyword: "rate", title: "Add rate limiting requirements", category: "security" }, { keyword: "log", title: "Add logging and observability requirements", category: "quality" }, { keyword: "error", title: "Add error handling and recovery requirements", category: "quality" }, { keyword: "test", title: "Add testing strategy requirements", category: "coverage" }, { keyword: "doc", title: "Add documentation requirements", category: "improvement" }, { keyword: "config", title: "Add configuration management requirements", category: "architecture" }, ]; for (const cat of commonCategories) { if (!specText.includes(cat.keyword)) { ideas.push({ id: nextIdeaId(), source: "spec_missing", category: cat.category, title: cat.title, rationale: `No mention of "${cat.keyword}" in the project specification. This is a common requirement category that may be missing.`, confidence: 0.55, actions: ["add_requirement"], tier: "mechanical", }); } } return ideas; } private readGitLessons(): Array<{ topic: string; detail: string }> { const lessons: Array<{ topic: string; detail: string }> = []; try { const log = execSync('git log --all --grep="lessons:" --format="%B" -50', { cwd: this.projectPath, encoding: "utf-8", timeout: 5000, }); const lessonsRegex = /lessons:\s*\n((?:\s+-\s+.+\n?)+)/g; let match; while ((match = lessonsRegex.exec(log)) !== null) { const items = match[1].split("\n").filter((l: string) => l.trim().startsWith("-")); for (const item of items) { const detail = item.replace(/^\s*-\s*/, "").trim(); const topic = detail.split(":")[0].trim().toLowerCase(); lessons.push({ topic, detail }); } } } catch {} return lessons; } runAffected(): Idea[] { resetIdeaCounter(); const ideas: Idea[] = []; try { const diff = execSync("git diff --name-only HEAD", { cwd: this.projectPath, encoding: "utf-8", timeout: 5000, }); const changedFiles = diff.trim().split("\n").filter(Boolean); if (changedFiles.length === 0) return ideas; const archMd = this.ciFiles.readArchitectureMd(); if (!archMd) return ideas; for (const changedFile of changedFiles) { const parts = changedFile.split("/").filter(Boolean); const srcIdx = parts.indexOf("src"); if (srcIdx >= 0 && parts.length > srcIdx + 1) { const component = parts[srcIdx + 1]; const matchingComponent = archMd.components.find( (c) => c.name.toLowerCase().replace(/\s+/g, "-") === component.toLowerCase() ); if (matchingComponent) { for (const dep of matchingComponent.dependsOn) { ideas.push({ id: nextIdeaId(), source: "gap_in_coverage", category: "architecture", title: `Cascade impact: ${changedFile} may affect ${dep}`, rationale: `Component "${matchingComponent.name}" (which depends on "${dep}") was modified. Verify that "${dep}" still works correctly.`, confidence: 0.7, actions: ["add_test"], tier: "mechanical", }); } } } } } catch {} return ideas; } runExternal(): Idea[] { resetIdeaCounter(); const ideas: Idea[] = []; try { const auditResult = execSync("npm audit --json 2>/dev/null || echo '{}'", { cwd: this.projectPath, encoding: "utf-8", timeout: 30000, }); const audit = JSON.parse(auditResult); const vulnerabilities = audit.vulnerabilities || {}; for (const [pkg, info] of Object.entries(vulnerabilities as Record)) { const severity = info.severity || "unknown"; if (severity === "high" || severity === "critical") { ideas.push({ id: nextIdeaId(), source: "external_signal", category: "security", title: `${severity.toUpperCase()} vulnerability in ${pkg}`, rationale: `npm audit reports a ${severity} severity vulnerability in "${pkg}". This should be addressed immediately.`, confidence: 0.95, actions: ["add_security_pattern", "update_roadmap"], tier: "mechanical", }); } else if (severity === "moderate") { ideas.push({ id: nextIdeaId(), source: "external_signal", category: "security", title: `Moderate vulnerability in ${pkg}`, rationale: `npm audit reports a moderate severity vulnerability in "${pkg}". Consider upgrading this dependency.`, confidence: 0.8, actions: ["add_security_pattern"], tier: "mechanical", }); } } } catch {} try { const result = execSync("npm outdated --json 2>/dev/null || echo '{}'", { cwd: this.projectPath, encoding: "utf-8", timeout: 15000, }); const outdated = JSON.parse(result); let staleCount = 0; for (const pkg of Object.keys(outdated)) { staleCount++; } if (staleCount > 5) { ideas.push({ id: nextIdeaId(), source: "external_signal", category: "quality", title: `${staleCount} outdated dependencies`, rationale: `${staleCount} packages are outdated. Consider scheduling a dependency upgrade task.`, confidence: 0.6, actions: ["update_roadmap"], tier: "mechanical", }); } } catch {} return ideas; } runCrossProject(): Idea[] { resetIdeaCounter(); const ideas: Idea[] = []; const projects = this.ciFiles.listProjects(); if (projects.length <= 1) return ideas; const currentSlug = this.ciFiles.getProjectSlug() || projects[0].slug; for (const project of projects) { if (project.slug === currentSlug) continue; const projectDir = path.join(this.projectPath, ".ciagent", project.slug); if (!fs.existsSync(projectDir)) continue; try { const log = execSync( `git log --all --grep="lessons:" --format="%B" -20`, { cwd: this.projectPath, encoding: "utf-8", timeout: 5000 } ); const lessonsRegex = /lessons:\s*\n((?:\s+-\s+.+\n?)+)/g; let match; while ((match = lessonsRegex.exec(log)) !== null) { const items = match[1].split("\n").filter((l: string) => l.trim().startsWith("-")); for (const item of items) { const detail = item.replace(/^\s*-\s*/, "").trim(); const topic = detail.split(":")[0].trim(); ideas.push({ id: nextIdeaId(), source: "cross_project_lesson", category: "improvement", title: `Cross-project lesson from ${project.slug}: ${topic}`, rationale: `Project "${project.slug}" learned: "${detail}". Consider whether this applies to the current project.`, confidence: 0.6, actions: ["add_requirement"], tier: "cross-project", }); } } } catch {} } return ideas; } formatIdeas(ideas: Idea[]): string { if (ideas.length === 0) return "No improvement ideas identified for this project."; const lines: string[] = ["Improvement Ideas:", ""]; for (const idea of ideas) { lines.push(`[${idea.source}|${idea.confidence.toFixed(2)}] ${idea.title} — ${idea.rationale}${idea.relatedReq ? ` (req: ${idea.relatedReq})` : ""}`); } return lines.join("\n"); } formatIdeasJson(ideas: Idea[]): IdeationResult { const byCategory: Record = {}; const byTier: Record = {}; for (const idea of ideas) { byCategory[idea.category] = (byCategory[idea.category] || 0) + 1; byTier[idea.tier] = (byTier[idea.tier] || 0) + 1; } return { project: this.ciFiles.getProjectSlug() || "default", milestone: "", ideas, summary: { total: ideas.length, accepted: 0, skipped: 0, by_category: byCategory, by_tier: byTier, }, }; } acceptIdea(idea: Idea): { reqId: string; addedToRequirements: boolean; addedToRoadmap: boolean } { const reqId = idea.id; const reqs = this.ciFiles.readRequirementsMd(); const roadmap = this.ciFiles.readRoadmapMd(); let addedToRequirements = false; let addedToRoadmap = false; if (reqs) { const categoryMap: Record = { security: "Security", quality: "Quality", architecture: "Architecture", coverage: "Coverage", improvement: "Improvement", spec: "Specification", chaos: "Resilience", }; const categoryName = categoryMap[idea.category] || "Improvement"; let foundCategory = false; for (const cat of reqs.v1) { if (cat.category.toLowerCase() === categoryName.toLowerCase()) { cat.items.push({ id: reqId, description: idea.title }); foundCategory = true; break; } } if (!foundCategory) { reqs.v1.push({ category: categoryName, items: [{ id: reqId, description: idea.title }], }); } reqs.traceability.push({ requirement: reqId, phase: 0, status: "pending", }); this.ciFiles.writeRequirementsMd(reqs); addedToRequirements = true; } if (roadmap) { const lastPhase = roadmap.phases.length > 0 ? roadmap.phases[roadmap.phases.length - 1] : null; const nextPhaseNumber = lastPhase ? lastPhase.number + 1 : 1; const phaseName = idea.category === "security" ? "security-hardening" : idea.category === "architecture" ? "architecture-fix" : idea.category === "coverage" ? "coverage-expansion" : idea.category === "quality" ? "quality-improvement" : idea.category === "spec" ? "spec-refinement" : idea.category === "chaos" ? "resilience-hardening" : "improvement"; roadmap.phases.push({ number: nextPhaseNumber, name: phaseName, description: idea.title, status: "not_started", dependsOn: lastPhase ? [lastPhase.number] : [], requirements: [reqId], successCriteria: [idea.rationale], }); this.ciFiles.writeRoadmapMd(roadmap); addedToRoadmap = true; } return { reqId, addedToRequirements, addedToRoadmap }; } acceptIdeas(ideas: Idea[]): { accepted: Idea[]; results: Array<{ reqId: string; addedToRequirements: boolean; addedToRoadmap: boolean }> } { const accepted: Idea[] = []; const results: Array<{ reqId: string; addedToRequirements: boolean; addedToRoadmap: boolean }> = []; for (const idea of ideas) { const result = this.acceptIdea(idea); if (result.addedToRequirements || result.addedToRoadmap) { accepted.push(idea); results.push(result); } } return { accepted, results }; } }