import { Injectable } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service.js"; import { SOURCE_PRIORITY } from "./constants.js"; // M-8 跨章節整合:共用年表排序(Scene.storyOrder 本身即年表鍵,不需要另外的服務排序)、 // 矛盾偵測與裁決優先序(小說原文 > 官方設定集 > 動畫改編),無法裁決者標記待人工確認。 @Injectable() export class TimelineService { constructor(private readonly prisma: PrismaService) {} // 掃描同一事件標籤下、同一事實鍵卻不同事實值的場景——這是故意置入矛盾最容易驗證的形式。 async detectContradictions(workId: string): Promise { const facts = await this.prisma.client.sceneFact.findMany({ where: { scene: { workId } }, include: { scene: true }, }); const groups = new Map(); for (const fact of facts) { const key = `${fact.eventLabel}::${fact.factKey}`; const group = groups.get(key) ?? []; group.push(fact); groups.set(key, group); } let created = 0; for (const [key, group] of groups) { const distinctValues = new Set(group.map((fact) => fact.factValue)); if (distinctValues.size <= 1) { continue; } const [eventLabel, factKey] = key.split("::"); const existing = await this.prisma.client.sceneContradiction.findFirst({ where: { workId, eventLabel, factKey } }); if (existing) { continue; } const sorted = [...group].sort( (a, b) => (SOURCE_PRIORITY[a.scene.sourceType] ?? 99) - (SOURCE_PRIORITY[b.scene.sourceType] ?? 99), ); const best = sorted[0]; const runnerUpDifferentValue = sorted.find((fact) => fact.factValue !== best.factValue); const canAutoResolve = runnerUpDifferentValue !== undefined && (SOURCE_PRIORITY[best.scene.sourceType] ?? 99) < (SOURCE_PRIORITY[runnerUpDifferentValue.scene.sourceType] ?? 99); await this.prisma.client.sceneContradiction.create({ data: { workId, eventLabel, factKey, description: `「${eventLabel}」的「${factKey}」出現不同描述:${[...distinctValues].join(" vs ")}`, status: canAutoResolve ? "RESOLVED" : "PENDING_HUMAN_REVIEW", resolution: canAutoResolve ? `依裁決優先序(小說原文 > 官方設定集 > 動畫改編)採用「${best.factValue}」(來源:${best.scene.sourceType})` : null, }, }); created += 1; } return created; } async listContradictions(workId: string) { return this.prisma.client.sceneContradiction.findMany({ where: { workId } }); } // T39:detectContradictions() 只能自動裁決「來源優先序不同」的矛盾,剩下標記 PENDING_HUMAN_REVIEW // 的矛盾原本沒有任何端點可以真的解決掉——這是本次補上的缺口,否則含這類矛盾的作品會永久卡住, // 沒有任何路徑能讓後續場景繼續投影。 async resolveContradiction(contradictionId: string, resolution: string): Promise { await this.prisma.client.sceneContradiction.update({ where: { id: contradictionId }, data: { status: "RESOLVED", resolution }, }); } }