198 lines
5.9 KiB
TypeScript
198 lines
5.9 KiB
TypeScript
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<string, Set<string>> = new Map(); // category -> attributes from B
|
||
const attributesForB: Map<string, Set<string>> = 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<string, Set<string>>,
|
||
_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<string, CrossoverPair[]>);
|
||
|
||
// 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`;
|
||
}
|