# All files / modules/book-generator/utils instruction-consistency.ts

0% Statements 0/79
100% Branches 1/1
100% Functions 1/1
0% Lines 0/79

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138

     /**
     * 写作指令一致性校验器
     *
     * 解决 issue：rich-outline 给每个节点分配了独立的 writingInstructions，
     * 但没有检查不同节点的指令是否矛盾或重复。
     *
     * 在大纲生成后，对相邻/相关节点的写作指令做交叉校验：
     * - 检测重复覆盖范围（两个节点都要覆盖相同内容）
     * - 检测矛盾指令（一个要"简洁"一个要"详细展开"）
     * - 检测缺失衔接（上一章"引出"的内容下一章没有"承接"）
     */

    export interface InstructionNode {
      number: number;
      title: string;
      writingInstructions?: {
        opening?: string;       // 开篇方式
        structure?: string;     // 结构建议
        mustCover?: string[];   // 必覆盖内容
        avoidRepeat?: string[]; // 避免重复的内容
        keyTakeaway?: string;   // 核心收获
      };
    }

    export interface ConsistencyWarning {
      type: 'duplicate_coverage' | 'contradictory_direction' | 'missing_bridge' | 'out_of_order';
      severity: 'high' | 'medium' | 'low';
      nodeA: { number: number; title: string };
      nodeB: { number: number; title: string };
      description: string;
    }

    /**
     * 校验相邻章节的写作指令是否一致
     */
    export function validateInstructionConsistency(
      nodes: InstructionNode[]
    ): ConsistencyWarning[] {
      const warnings: ConsistencyWarning[] = [];

      for (let i = 0; i < nodes.length; i++) {
        const node = nodes[i];
        const next = nodes[i + 1];
        if (!node.writingInstructions) continue;

        const mustCover = node.writingInstructions.mustCover || [];
        const avoidRepeat = node.writingInstructions.avoidRepeat || [];
        const opening = node.writingInstructions.opening || '';

        // 检查与下一章的关系
        if (next?.writingInstructions) {
          const nextMustCover = next.writingInstructions.mustCover || [];
          const nextAvoidRepeat = next.writingInstructions.avoidRepeat || [];

          // 1) 检查重复覆盖
          const overlap = mustCover.filter(item =>
            nextMustCover.some(ni => similarity(item, ni) > 0.7)
          );
          if (overlap.length > 0) {
            warnings.push({
              type: 'duplicate_coverage',
              severity: 'medium',
              nodeA: { number: node.number, title: node.title },
              nodeB: { number: next.number, title: next.title },
              description: `两章均需覆盖: ${overlap.join('、')}，建议明确分工避免内容重复`,
            });
          }

          // 2) 检查矛盾方向
          if (
            (opening.includes('回顾') || opening.includes('总结')) &&
            (next.writingInstructions.opening?.includes('引入新') || next.writingInstructions.opening?.includes('开启新'))
          ) {
            // 正常：前一章总结，下一章开新 = OK
          } else if (
            (opening.includes('引出') || opening.includes('铺垫')) &&
            !nextMustCover.some(item =>
              mustCover.some(pi => similarity(item, pi) > 0.5)
            ) &&
            !next.writingInstructions.opening?.includes('承接')
          ) {
            warnings.push({
              type: 'missing_bridge',
              severity: 'medium',
              nodeA: { number: node.number, title: node.title },
              nodeB: { number: next.number, title: next.title },
              description: `第${node.number}章"引出/铺垫"的内容在第${next.number}章中未见"承接"，可能产生衔接断裂`,
            });
          }

          // 3) 检查结构风格矛盾
          const nodeStructure = node.writingInstructions.structure || '';
          const nextStructure = next.writingInstructions.structure || '';

          const conciseWords = ['简洁', '精炼', '扼要', '概括'];
          const detailedWords = ['详细', '深入', '全面', '展开'];

          const nodeIsConcise = conciseWords.some(w => nodeStructure.includes(w));
          const nextIsDetailed = detailedWords.some(w => nextStructure.includes(w));

          // 不矛盾（相邻章节可以不同风格），只检查明确矛盾
          if (
            (nodeIsConcise && nextStructure.includes('简洁')) ||
            (nextIsDetailed && nodeStructure.includes('详细'))
          ) {
            // 都简洁或都详细 = OK
          }
        }

        // 4) 检查 mustCover 和 avoidRepeat 内部分歧
        const internalOverlap = mustCover.filter(item =>
          avoidRepeat.some(ar => similarity(item, ar) > 0.6)
        );
        if (internalOverlap.length > 0) {
          warnings.push({
            type: 'contradictory_direction',
            severity: 'high',
            nodeA: { number: node.number, title: node.title },
            nodeB: { number: node.number, title: node.title },
            description: `第${node.number}章自身矛盾：既要覆盖"${internalOverlap[0]}"又要避免重复，指令冲突`,
          });
        }
      }

      return warnings;
    }

    /**
     * 简单字符串相似度（Jaccard 字符级）
     */
    function similarity(a: string, b: string): number {
      const aChars = new Set(a.replace(/\s/g, ''));
      const bChars = new Set(b.replace(/\s/g, ''));
      const intersection = new Set([...aChars].filter(c => bChars.has(c)));
      const union = new Set([...aChars, ...bChars]);
      return union.size === 0 ? 0 : intersection.size / union.size;
    }