|
@@ -0,0 +1,171 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * 回归测试:groupByDependencyLayer 应该保留所有叶节点
|
|
|
|
|
+ *
|
|
|
|
|
+ * 之前的 bug:assigned set 用 node.number 作 key,多个 L2 节点(number=1 或 2)被合并。
|
|
|
|
|
+ * 修复后:用 node.id 作唯一标识,叶节点按所属章 number 解析依赖。
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+// 直接复制修复后的函数(避免 import 整个模块的副作用)
|
|
|
|
|
+function groupByDependencyLayer(
|
|
|
|
|
+ targets: any[],
|
|
|
|
|
+ crossReferences: Record<string, { dependsOn: string[]; usedBy: string[] }> | undefined,
|
|
|
|
|
+ parentNumberByChapterId: Map<number, number>,
|
|
|
|
|
+): { layer: number; nodes: any[] }[] {
|
|
|
|
|
+ if (!crossReferences || Object.keys(crossReferences).length === 0) {
|
|
|
|
|
+ return [{ layer: 0, nodes: targets }];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const chapterNumOf = (n: any): number => {
|
|
|
|
|
+ if (n.level === 1) return n.number;
|
|
|
|
|
+ return parentNumberByChapterId.get(n.id) ?? 0;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ interface NodeDegree { node: any; dependsOn: number[]; }
|
|
|
|
|
+ const degrees: NodeDegree[] = targets.map(t => ({ node: t, dependsOn: [] }));
|
|
|
|
|
+
|
|
|
|
|
+ const degreesByChapter = (chapterNum: number, depNums: number[]) => {
|
|
|
|
|
+ for (const d of degrees) {
|
|
|
|
|
+ if (chapterNumOf(d.node) === chapterNum) {
|
|
|
|
|
+ d.dependsOn = Array.from(new Set([...d.dependsOn, ...depNums]));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ for (const [refKey, ref] of Object.entries(crossReferences)) {
|
|
|
|
|
+ const match = refKey.match(/ch(?:apter)?(\d+)/i);
|
|
|
|
|
+ if (!match) continue;
|
|
|
|
|
+ const num = parseInt(match[1]);
|
|
|
|
|
+ const depNums: number[] = [];
|
|
|
|
|
+ for (const dep of (ref.dependsOn || [])) {
|
|
|
|
|
+ const depMatch = dep.match(/ch(?:apter)?(\d+)/i);
|
|
|
|
|
+ if (depMatch) depNums.push(parseInt(depMatch[1]));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (depNums.length === 0) continue;
|
|
|
|
|
+ degreesByChapter(num, depNums);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const layers: any[][] = [];
|
|
|
|
|
+ const assignedIds = new Set<number>();
|
|
|
|
|
+ const isChapterNumAssigned = (num: number): boolean => {
|
|
|
|
|
+ return degrees.some(d => chapterNumOf(d.node) === num && assignedIds.has(d.node.id));
|
|
|
|
|
+ };
|
|
|
|
|
+ let changed = true;
|
|
|
|
|
+ while (changed) {
|
|
|
|
|
+ changed = false;
|
|
|
|
|
+ const currentLayer: any[] = [];
|
|
|
|
|
+ for (const d of degrees) {
|
|
|
|
|
+ if (assignedIds.has(d.node.id)) continue;
|
|
|
|
|
+ const allDepsAssigned = d.dependsOn.every(dep => isChapterNumAssigned(dep));
|
|
|
|
|
+ if (allDepsAssigned) {
|
|
|
|
|
+ currentLayer.push(d.node);
|
|
|
|
|
+ assignedIds.add(d.node.id);
|
|
|
|
|
+ changed = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (currentLayer.length > 0) layers.push(currentLayer);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const unassigned = targets.filter(t => !assignedIds.has(t.id));
|
|
|
|
|
+ if (unassigned.length > 0) layers.push(unassigned);
|
|
|
|
|
+
|
|
|
|
|
+ return layers.map((nodes, i) => ({ layer: i, nodes }));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 模拟 book 39 的数据
|
|
|
|
|
+// 6 个 L1 章节(L1 with id=104..109, number=1..6)
|
|
|
|
|
+// 12 个 L2 章节(每个 L1 下 2 个,L2 with number=1 或 2)
|
|
|
|
|
+const l1Chapters = [
|
|
|
|
|
+ { id: 104, level: 1, number: 1, parentId: 0, title: 'ch1' },
|
|
|
|
|
+ { id: 107, level: 1, number: 2, parentId: 0, title: 'ch2' },
|
|
|
|
|
+ { id: 110, level: 1, number: 3, parentId: 0, title: 'ch3' },
|
|
|
|
|
+ { id: 113, level: 1, number: 4, parentId: 0, title: 'ch4' },
|
|
|
|
|
+ { id: 116, level: 1, number: 5, parentId: 0, title: 'ch5' },
|
|
|
|
|
+ { id: 119, level: 1, number: 6, parentId: 0, title: 'ch6' },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const l2Chapters = [
|
|
|
|
|
+ // L1 ch1 (id=104) 下 2 个
|
|
|
|
|
+ { id: 105, level: 2, number: 1, parentId: 104, title: '1.1' },
|
|
|
|
|
+ { id: 106, level: 2, number: 2, parentId: 104, title: '1.2' },
|
|
|
|
|
+ // L1 ch2 (id=107) 下 2 个
|
|
|
|
|
+ { id: 108, level: 2, number: 1, parentId: 107, title: '2.1' },
|
|
|
|
|
+ { id: 109, level: 2, number: 2, parentId: 107, title: '2.2' },
|
|
|
|
|
+ // L1 ch3 (id=110) 下 2 个
|
|
|
|
|
+ { id: 111, level: 2, number: 1, parentId: 110, title: '3.1' },
|
|
|
|
|
+ { id: 112, level: 2, number: 2, parentId: 110, title: '3.2' },
|
|
|
|
|
+ // L1 ch4 (id=113) 下 2 个
|
|
|
|
|
+ { id: 114, level: 2, number: 1, parentId: 113, title: '4.1' },
|
|
|
|
|
+ { id: 115, level: 2, number: 2, parentId: 113, title: '4.2' },
|
|
|
|
|
+ // L1 ch5 (id=116) 下 2 个
|
|
|
|
|
+ { id: 117, level: 2, number: 1, parentId: 116, title: '5.1' },
|
|
|
|
|
+ { id: 118, level: 2, number: 2, parentId: 116, title: '5.2' },
|
|
|
|
|
+ // L1 ch6 (id=119) 下 2 个
|
|
|
|
|
+ { id: 120, level: 2, number: 1, parentId: 119, title: '6.1' },
|
|
|
|
|
+ { id: 121, level: 2, number: 2, parentId: 119, title: '6.2' },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+// parentNumberByChapterId 映射
|
|
|
|
|
+const parentNumberByChapterId = new Map<number, number>();
|
|
|
|
|
+for (const c of l1Chapters) parentNumberByChapterId.set(c.id, c.number);
|
|
|
|
|
+for (const c of l2Chapters) {
|
|
|
|
|
+ const parent = l1Chapters.find(p => p.id === c.parentId);
|
|
|
|
|
+ if (parent) parentNumberByChapterId.set(c.id, parent.number);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 模拟 crossReferences:ch2 依赖 ch1 的内容,ch3 依赖 ch2,ch4 依赖 ch3
|
|
|
|
|
+const crossReferences: Record<string, { dependsOn: string[]; usedBy: string[] }> = {
|
|
|
|
|
+ 'ch2': { dependsOn: ['ch1.认知心理学的定义'], usedBy: [] },
|
|
|
|
|
+ 'ch3': { dependsOn: ['ch2.注意的筛选机制'], usedBy: [] },
|
|
|
|
|
+ 'ch4': { dependsOn: ['ch3.记忆系统'], usedBy: [] },
|
|
|
|
|
+ 'ch5': { dependsOn: ['ch3.记忆'], usedBy: [] },
|
|
|
|
|
+ 'ch6': { dependsOn: ['ch3.概念与分类'], usedBy: [] },
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const targets = [...l2Chapters];
|
|
|
|
|
+const layers = groupByDependencyLayer(targets, crossReferences, parentNumberByChapterId);
|
|
|
|
|
+
|
|
|
|
|
+console.log('=== 测试 groupByDependencyLayer ===');
|
|
|
|
|
+console.log(`输入: ${targets.length} 个叶节点`);
|
|
|
|
|
+console.log(`输出: ${layers.length} 层`);
|
|
|
|
|
+let totalInLayers = 0;
|
|
|
|
|
+for (const l of layers) {
|
|
|
|
|
+ const ids = l.nodes.map(n => `${n.id}#${n.number}`).join(',');
|
|
|
|
|
+ console.log(` 层${l.layer} (${l.nodes.length} 节点): ${ids}`);
|
|
|
|
|
+ totalInLayers += l.nodes.length;
|
|
|
|
|
+}
|
|
|
|
|
+console.log(`总计: ${totalInLayers} 节点`);
|
|
|
|
|
+
|
|
|
|
|
+let pass = true;
|
|
|
|
|
+if (totalInLayers !== 12) {
|
|
|
|
|
+ console.log(`❌ 失败:预期 12 节点,实际 ${totalInLayers} 节点`);
|
|
|
|
|
+ pass = false;
|
|
|
|
|
+} else {
|
|
|
|
|
+ console.log('✅ 所有 12 个叶节点都被分配');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 验证属于 L1-ch1 的节点应在 layer 0(无依赖)
|
|
|
|
|
+const ch1Nodes = l2Chapters.filter(c => c.parentId === 104);
|
|
|
|
|
+const allAssignedIds = new Set<number>();
|
|
|
|
|
+for (const l of layers) for (const n of l.nodes) allAssignedIds.add(n.id);
|
|
|
|
|
+for (const n of ch1Nodes) {
|
|
|
|
|
+ if (!allAssignedIds.has(n.id)) {
|
|
|
|
|
+ console.log(`❌ 失败:L1-ch1 下的叶节点 ${n.id} 未被分配`);
|
|
|
|
|
+ pass = false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+if (pass) console.log('✅ L1-ch1 下的 2 个叶节点都被分配');
|
|
|
|
|
+
|
|
|
|
|
+// 验证 L1-ch3/4/5/6 下的节点都被分配(之前 bug 会丢节点)
|
|
|
|
|
+for (const parentL1 of [107, 110, 113, 116, 119]) {
|
|
|
|
|
+ const ch = l2Chapters.filter(c => c.parentId === parentL1);
|
|
|
|
|
+ for (const n of ch) {
|
|
|
|
|
+ if (!allAssignedIds.has(n.id)) {
|
|
|
|
|
+ console.log(`❌ 失败:L1 父 ${parentL1} 下的叶节点 ${n.id} 未被分配`);
|
|
|
|
|
+ pass = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+if (pass) console.log('✅ L1-ch2/3/4/5/6 下的所有 10 个叶节点都被分配(关键 bug 修复)');
|
|
|
|
|
+
|
|
|
|
|
+if (!pass) process.exit(1);
|
|
|
|
|
+console.log('\n=== ✅ 所有测试通过 ===');
|