UNPKG

@autobe/agent

Version:

AI backend server code generator

173 lines (147 loc) 4.95 kB
// ─── Types ─── /** * A single extracted decision from one file's section content. * * Decisions are binary/discrete choices embedded in prose, e.g.: * * - "password change requires current password" → topic: "password_change", * decision: "requires_current_password", value: "yes" * - "deleted email can be reused" → topic: "deleted_email", decision: * "can_be_reused", value: "yes" */ export interface IExtractedDecision { /** Normalized topic grouping (e.g., "password_change", "email_reuse") */ topic: string; /** Specific decision within the topic (e.g., "requires_current_password") */ decision: string; /** The value of the decision (e.g., "yes", "no", "soft_delete", "hard_delete") */ value: string; /** Evidence quote from the source text */ evidence: string; } /** Decisions extracted from a single file, returned by the extraction LLM. */ export interface IFileDecisions { /** The filename this was extracted from */ filename: string; /** All decisions extracted from this file */ decisions: IExtractedDecision[]; } /** * A conflict between two or more files that state different values for the same * topic+decision. */ export interface IDecisionConflict { /** The topic of the conflict */ topic: string; /** The specific decision that conflicts */ decision: string; /** All differing values with their source files and evidence */ values: Array<{ value: string; files: string[]; evidence: string; }>; } // ─── Main Detection ─── /** * Detect decision-level conflicts across all files. * * Groups all extracted decisions by `topic + decision` key, then finds cases * where different files assign different values to the same key. * * This catches prose-level contradictions like: * * - File A: "password change requires current password" (yes) * - File B: "password change does not require current password" (no) */ export const detectDecisionConflicts = (props: { fileDecisions: IFileDecisions[]; }): IDecisionConflict[] => { // Group by topic+decision const groups: Map< string, Array<{ value: string; filename: string; evidence: string }> > = new Map(); for (const { filename, decisions } of props.fileDecisions) { for (const d of decisions) { const key = `${normalizeKey(d.topic)}::${normalizeKey(d.decision)}`; if (!groups.has(key)) groups.set(key, []); groups.get(key)!.push({ value: normalizeValue(d.value), filename, evidence: d.evidence, }); } } // Find conflicts: same key, different values const conflicts: IDecisionConflict[] = []; for (const [key, entries] of groups) { // Group entries by value const byValue: Map< string, Array<{ filename: string; evidence: string }> > = new Map(); for (const entry of entries) { if (!byValue.has(entry.value)) byValue.set(entry.value, []); byValue.get(entry.value)!.push({ filename: entry.filename, evidence: entry.evidence, }); } // If more than one distinct value exists → conflict if (byValue.size <= 1) continue; const [topic, decision] = key.split("::"); conflicts.push({ topic: topic!, decision: decision!, values: [...byValue.entries()].map(([value, sources]) => ({ value, files: sources.map((s) => s.filename), evidence: sources[0]?.evidence ?? "", })), }); } return conflicts; }; /** * Build a map from filename → list of decision conflict feedback strings. * * Each file involved in a conflict gets feedback describing the contradiction. */ export const buildFileDecisionConflictMap = ( conflicts: IDecisionConflict[], ): Map<string, string[]> => { const map: Map<string, string[]> = new Map(); for (const conflict of conflicts) { const valueSummary = conflict.values .map((v) => `"${v.value}" in [${v.files.join(", ")}]`) .join(" vs "); const feedback = `Decision conflict: ${conflict.topic}.${conflict.decision} — ${valueSummary}. ` + `Files must agree on this decision. Align with the canonical source.`; // Add feedback to ALL files involved in this conflict for (const valueGroup of conflict.values) { for (const filename of valueGroup.files) { if (!map.has(filename)) map.set(filename, []); map.get(filename)!.push(feedback); } } } return map; }; // ─── Helpers ─── /** * Normalize a key string for grouping: lowercase, replace whitespace/special * chars with underscore, trim. */ function normalizeKey(s: string): string { return s .toLowerCase() .trim() .replace(/[\s\-\.]+/g, "_") .replace(/[^a-z0-9_]/g, ""); } /** Normalize a value for comparison: lowercase, trim, collapse whitespace. */ function normalizeValue(s: string): string { return s.toLowerCase().trim().replace(/\s+/g, " "); }