70ee21856d
---ci---
phase: 2
milestone: v0.10
status: execute
decisions:
- id: D-087
decision: All 6 innovative features in v1 (pattern mining, drift detection, layer inversion, cross-project, chaos, spec)
rationale: User wants bleeding-edge; all uniquely differentiated
confidence: 0.82
requirements:
covered:
- IDEATE-04
- IDEATE-05
- IDEATE-06
- IDEATE-09
- IDEATE-10
---/ci---
- IDEATE-04: Verification layer inversion (structural, behavioral, security, quality missing detection)
- IDEATE-05: Architectural drift detection (documented vs actual component comparison)
- IDEATE-06: Spec-driven improvement (ambiguity detection, missing category detection)
- IDEATE-09: Backend-enriched analysis (prioritization, novel suggestions, action plans)
- IDEATE-10: Chaos engineering ideation (backend unavailable, requirement change, coverage drop)
- Deduplicated type exports: IdeationSource/Idea/etc now in types/ideation.ts
- 538 tests passing
1012 lines
34 KiB
TypeScript
1012 lines
34 KiB
TypeScript
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";
|
|
import { loadConfig } from "./config.js";
|
|
import {
|
|
IdeationSource,
|
|
IdeationCategory,
|
|
IdeationAction,
|
|
IdeationTier,
|
|
Idea,
|
|
IdeationResult,
|
|
IdeationSummary,
|
|
IdeationConfig,
|
|
DEFAULT_IDEATION_CONFIG,
|
|
} from "../types/ideation.js";
|
|
|
|
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<string>();
|
|
for (const t of reqs.traceability) {
|
|
if (t.status === "complete") {
|
|
coveredReqs.add(t.requirement);
|
|
}
|
|
}
|
|
|
|
const allReqIds = new Set<string>();
|
|
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<string, number> = {};
|
|
const topicDetails: Record<string, string[]> = {};
|
|
|
|
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<string, number> = {};
|
|
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<string, number> = {};
|
|
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<string>();
|
|
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<string, { severity?: string; title?: string }>)) {
|
|
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;
|
|
}
|
|
|
|
runBackendEnriched(mechanicalIdeas: Idea[], context?: string): Idea[] {
|
|
resetIdeaCounter();
|
|
const enriched: Idea[] = [];
|
|
|
|
const prioritized = this.prioritizeMechanicalFindings(mechanicalIdeas);
|
|
enriched.push(...prioritized);
|
|
|
|
const novelSuggestions = this.suggestNovelImprovements(mechanicalIdeas);
|
|
enriched.push(...novelSuggestions);
|
|
|
|
const chaosScenarios = this.generateChaosScenarios();
|
|
enriched.push(...chaosScenarios);
|
|
|
|
return enriched.slice(0, DEFAULT_IDEATION_CONFIG.max_ideas);
|
|
}
|
|
|
|
private prioritizeMechanicalFindings(mechanicalIdeas: Idea[]): Idea[] {
|
|
const categoryPriority: Record<string, number> = {
|
|
security: 1.0,
|
|
coverage: 0.9,
|
|
architecture: 0.8,
|
|
quality: 0.7,
|
|
improvement: 0.6,
|
|
spec: 0.5,
|
|
chaos: 0.4,
|
|
};
|
|
|
|
const sourceBoost: Record<string, number> = {
|
|
escalation_pattern: 0.15,
|
|
repeated_lesson: 0.1,
|
|
uncovered_requirement: 0.1,
|
|
compound_pattern: 0.08,
|
|
low_confidence_decision: 0.05,
|
|
architecture_drift: 0.05,
|
|
verification_inversion: 0.03,
|
|
};
|
|
|
|
return mechanicalIdeas
|
|
.map((idea) => ({
|
|
...idea,
|
|
confidence: Math.min(
|
|
idea.confidence + (categoryPriority[idea.category] || 0) * 0.1 + (sourceBoost[idea.source] || 0),
|
|
1.0
|
|
),
|
|
tier: "backend-enriched" as IdeationTier,
|
|
}))
|
|
.sort((a, b) => b.confidence - a.confidence);
|
|
}
|
|
|
|
private suggestNovelImprovements(mechanicalIdeas: Idea[]): Idea[] {
|
|
const ideas: Idea[] = [];
|
|
const hasCategories = new Set(mechanicalIdeas.map((i) => i.category));
|
|
|
|
if (!hasCategories.has("security")) {
|
|
ideas.push({
|
|
id: nextIdeaId(),
|
|
source: "improvement_pattern",
|
|
category: "security",
|
|
title: "Consider adding security threat modeling",
|
|
rationale: "No security-related ideas were identified. Projects without explicit security analysis often have blind spots in threat modeling.",
|
|
confidence: 0.55,
|
|
actions: ["add_security_pattern"],
|
|
tier: "backend-enriched",
|
|
});
|
|
}
|
|
|
|
if (!hasCategories.has("quality")) {
|
|
ideas.push({
|
|
id: nextIdeaId(),
|
|
source: "improvement_pattern",
|
|
category: "quality",
|
|
title: "Consider establishing quality baselines",
|
|
rationale: "No quality-related ideas were identified. Adding code quality baselines and review standards can prevent regressions.",
|
|
confidence: 0.50,
|
|
actions: ["add_test", "update_roadmap"],
|
|
tier: "backend-enriched",
|
|
});
|
|
}
|
|
|
|
if (!hasCategories.has("architecture")) {
|
|
ideas.push({
|
|
id: nextIdeaId(),
|
|
source: "improvement_pattern",
|
|
category: "architecture",
|
|
title: "Review architectural boundaries and dependencies",
|
|
rationale: "No architecture drift detected, but periodic boundary review is a best practice for healthy codebases.",
|
|
confidence: 0.45,
|
|
actions: ["update_architecture"],
|
|
tier: "backend-enriched",
|
|
});
|
|
}
|
|
|
|
return ideas;
|
|
}
|
|
|
|
generateChaosScenarios(): Idea[] {
|
|
resetIdeaCounter();
|
|
const ideas: Idea[] = [];
|
|
const config = this.getIdeationConfig();
|
|
|
|
if (!config.chaos.enabled) return ideas;
|
|
|
|
for (const scenario of config.chaos.scenarios) {
|
|
switch (scenario) {
|
|
case "backend_unavailable":
|
|
ideas.push({
|
|
id: nextIdeaId(),
|
|
source: "chaos_scenario",
|
|
category: "chaos",
|
|
title: "Resilience: Handle backend unavailability",
|
|
rationale: "What if the primary intelligence backend is unavailable? Add fallback paths and graceful degradation for all backend-dependent features.",
|
|
confidence: 0.7,
|
|
actions: ["add_requirement", "add_test"],
|
|
tier: "backend-enriched",
|
|
});
|
|
break;
|
|
case "requirement_change":
|
|
ideas.push({
|
|
id: nextIdeaId(),
|
|
source: "chaos_scenario",
|
|
category: "chaos",
|
|
title: "Resilience: Handle mid-implementation requirement changes",
|
|
rationale: "What if requirements change during implementation? Design for adaptability with clear interfaces and minimal coupling.",
|
|
confidence: 0.65,
|
|
actions: ["add_requirement"],
|
|
tier: "backend-enriched",
|
|
});
|
|
break;
|
|
case "test_coverage_drop":
|
|
ideas.push({
|
|
id: nextIdeaId(),
|
|
source: "chaos_scenario",
|
|
category: "chaos",
|
|
title: "Resilience: Prevent test coverage regression",
|
|
rationale: "What if test coverage drops below threshold? Add CI gates that enforce minimum coverage and alert on regression.",
|
|
confidence: 0.75,
|
|
actions: ["add_test", "update_roadmap"],
|
|
tier: "backend-enriched",
|
|
});
|
|
break;
|
|
default:
|
|
ideas.push({
|
|
id: nextIdeaId(),
|
|
source: "chaos_scenario",
|
|
category: "chaos",
|
|
title: `Resilience: Handle ${scenario} scenario`,
|
|
rationale: `Consider adding resilience measures for the "${scenario}" failure scenario.`,
|
|
confidence: 0.5,
|
|
actions: ["add_requirement"],
|
|
tier: "backend-enriched",
|
|
});
|
|
}
|
|
}
|
|
|
|
return ideas;
|
|
}
|
|
|
|
private getIdeationConfig(): IdeationConfig {
|
|
try {
|
|
const config = loadConfig(this.projectPath);
|
|
return (config as any).ideation || DEFAULT_IDEATION_CONFIG;
|
|
} catch {
|
|
return DEFAULT_IDEATION_CONFIG;
|
|
}
|
|
}
|
|
|
|
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<string, number> = {};
|
|
const byTier: Record<string, number> = {};
|
|
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<string, string> = {
|
|
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 };
|
|
}
|
|
} |