import type { CrossoverPair, AttributeDAG, CategoryDefinition, DAGNode } from '../types'; /** * Result of crossover transformation - two separate DAGs * Each path receives attributes from the other path */ export interface CrossoverDAGResult { // Path A receives attributes from Path B pathA: AttributeDAG; // Path B receives attributes from Path A pathB: AttributeDAG; } /** * Convert selected crossover pairs into two separate AttributeDAGs. * * The crossover logic: * - When pair (A's "防水布" × B's "鋁合金") is selected: * - Path A gets "鋁合金" (from B) to transform with A's context * - Path B gets "防水布" (from A) to transform with B's context * * This allows experts to think about: * - "What if umbrella had aluminum alloy?" (A gets B's attribute) * - "What if bicycle had waterproof fabric?" (B gets A's attribute) */ export function crossoverPairsToDAGs( pairs: CrossoverPair[], dagA: AttributeDAG, dagB: AttributeDAG ): CrossoverDAGResult { // Collect attributes that each path receives from the other const attributesForA: Map> = new Map(); // category -> attributes from B const attributesForB: Map> = new Map(); // category -> attributes from A for (const pair of pairs) { // Path A receives the target node (from B) // Path B receives the source node (from A) if (pair.sourcePathId === 'A' && pair.targetPathId === 'B') { // A's attribute crossed with B's attribute // A gets B's attribute, B gets A's attribute const categoryForA = pair.targetNode.category; const categoryForB = pair.sourceNode.category; if (!attributesForA.has(categoryForA)) attributesForA.set(categoryForA, new Set()); if (!attributesForB.has(categoryForB)) attributesForB.set(categoryForB, new Set()); attributesForA.get(categoryForA)!.add(pair.targetNode.name); attributesForB.get(categoryForB)!.add(pair.sourceNode.name); } else if (pair.sourcePathId === 'B' && pair.targetPathId === 'A') { // B's attribute crossed with A's attribute // A gets B's attribute, B gets A's attribute (reverse direction) const categoryForA = pair.sourceNode.category; const categoryForB = pair.targetNode.category; if (!attributesForA.has(categoryForA)) attributesForA.set(categoryForA, new Set()); if (!attributesForB.has(categoryForB)) attributesForB.set(categoryForB, new Set()); attributesForA.get(categoryForA)!.add(pair.sourceNode.name); attributesForB.get(categoryForB)!.add(pair.targetNode.name); } } // Build DAG for Path A (receives attributes from B) const pathAResult = buildCrossoverDAG( dagA.query, attributesForA, `Crossover: ${dagB.query} → ${dagA.query}` ); // Build DAG for Path B (receives attributes from A) const pathBResult = buildCrossoverDAG( dagB.query, attributesForB, `Crossover: ${dagA.query} → ${dagB.query}` ); return { pathA: pathAResult, pathB: pathBResult, }; } /** * Build an AttributeDAG from crossover attributes */ function buildCrossoverDAG( originalQuery: string, attributesByCategory: Map>, _crossoverDescription: string ): AttributeDAG { const categories: CategoryDefinition[] = []; const nodes: DAGNode[] = []; let nodeIndex = 0; let categoryIndex = 0; for (const [categoryName, attributes] of attributesByCategory.entries()) { categories.push({ name: categoryName, description: `Crossover attributes for ${categoryName}`, is_fixed: false, order: categoryIndex, }); categoryIndex++; for (const attrName of attributes) { nodes.push({ id: `crossover-${nodeIndex}`, name: attrName, category: categoryName, order: nodeIndex, }); nodeIndex++; } } return { query: originalQuery, categories, nodes, edges: [], }; } /** * Legacy function - converts to single DAG (deprecated) * Kept for backwards compatibility */ export function crossoverPairsToDAG( pairs: CrossoverPair[], queryA: string, queryB: string ): AttributeDAG { // Group pairs by crossType const pairsByType = pairs.reduce((acc, pair) => { if (!acc[pair.crossType]) acc[pair.crossType] = []; acc[pair.crossType].push(pair); return acc; }, {} as Record); // Create categories from crossTypes const categories: CategoryDefinition[] = Object.keys(pairsByType).map((crossType, index) => ({ name: formatCrossTypeName(crossType), description: getCrossTypeDescription(crossType), is_fixed: false, order: index, })); // Create nodes from pairs const nodes: DAGNode[] = []; let nodeIndex = 0; for (const [crossType, typePairs] of Object.entries(pairsByType)) { const categoryName = formatCrossTypeName(crossType); for (const pair of typePairs) { nodes.push({ id: `crossover-${nodeIndex}`, name: `${pair.sourceNode.name} × ${pair.targetNode.name}`, category: categoryName, order: nodeIndex, }); nodeIndex++; } } return { query: `${queryA} × ${queryB}`, categories, nodes, edges: [], }; } function formatCrossTypeName(crossType: string): string { if (crossType.startsWith('same-')) { const category = crossType.replace('same-', ''); return `Same: ${category}`; } if (crossType.startsWith('cross-')) { const parts = crossType.replace('cross-', '').split('-'); if (parts.length >= 2) { return `Cross: ${parts[0]} × ${parts.slice(1).join('-')}`; } } return crossType; } function getCrossTypeDescription(crossType: string): string { if (crossType.startsWith('same-')) { const category = crossType.replace('same-', ''); return `Crossover pairs from the same category: ${category}`; } if (crossType.startsWith('cross-')) { return `Crossover pairs from different categories`; } return `Crossover pairs`; }