chore: save local changes

This commit is contained in:
2026-01-05 22:32:08 +08:00
parent bc281b8e0a
commit ec48709755
42 changed files with 5576 additions and 254 deletions

View File

@@ -0,0 +1,197 @@
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`;
}