diff --git a/src/app/actions/create-run.ts b/src/app/actions/create-run.ts index 7d08d63..7c5ecde 100644 --- a/src/app/actions/create-run.ts +++ b/src/app/actions/create-run.ts @@ -16,6 +16,8 @@ export async function createMockRunAction( _prevState: CreateRunFormState, formData: FormData, ): Promise { + // DB列名互換のためフォーム名はquestionを維持。 + // UI上はResearch Topicとして扱い、既存API/永続化互換を壊さない。 const raw = formData.get("question"); if (typeof raw !== "string" || !raw.trim()) { return { error: "Research topic is required." }; @@ -31,6 +33,8 @@ export async function createMockRunAction( return { error: manualSourceUrlsResult.message }; } + // runはowner scopeで分離する。未認証で作成させないことで + // 履歴閲覧/共有リンク管理の境界を単純に保つ。 const currentUser = await getCurrentUser(); if (!currentUser) { return { error: "Sign in is required to start an investigation." }; diff --git a/src/app/share/[token]/page.tsx b/src/app/share/[token]/page.tsx index b75b51b..cd7478c 100644 --- a/src/app/share/[token]/page.tsx +++ b/src/app/share/[token]/page.tsx @@ -12,6 +12,8 @@ type SharePageProps = { params: Promise<{ token: string }>; }; +// 共有ページはトークン知識ベースの限定公開。 +// 検索エンジン経由の露出を避けるため noindex/nofollow を強制する。 export const metadata = { robots: { index: false, @@ -87,6 +89,7 @@ export default async function SharePage({ params }: SharePageProps) { const run = shareLink.analysisRun; const { answer, sources } = selectLatestAnswerSnapshotForView(run.answerSnapshots); + // 共有閲覧者には再実行権限がないため、失敗時は安全文言のみを表示する。 if (run.status === "failed") { return (
diff --git a/src/features/run/components/run-result-view.tsx b/src/features/run/components/run-result-view.tsx index d5da320..cc75040 100644 --- a/src/features/run/components/run-result-view.tsx +++ b/src/features/run/components/run-result-view.tsx @@ -186,6 +186,8 @@ function edgeEndpoints( }; } +// TraceMapはチャット時系列ではなく調査導線を重視するため、 +// Mission -> Evidence -> Unknown -> Lineage -> Report の順を固定表示する。 const INVESTIGATION_GUIDE_STEPS = [ "Mission", "Evidence Map", @@ -210,6 +212,8 @@ export function RunResultView({ answerModel, answerGeneratedAt, }: RunResultViewProps) { + // グラフ/クレーム/ソース詳細を連動させる選択状態。 + // どれかを起点にしても同じ調査対象へフォーカスできるようにする。 const [selectedSourceId, setSelectedSourceId] = useState(null); const [selectedGraphNodeId, setSelectedGraphNodeId] = useState( null, diff --git a/src/features/run/lib/build-source-lineage.ts b/src/features/run/lib/build-source-lineage.ts index 039db7e..84d421b 100644 --- a/src/features/run/lib/build-source-lineage.ts +++ b/src/features/run/lib/build-source-lineage.ts @@ -83,6 +83,8 @@ function normalizeVerificationStatus(value: string | null | undefined): string | return value === "verified" ? "verified" : value ?? null; } +// Source Lineage Lite は厳密な出典系譜DBではなく、 +// sourceType / primary判定 / publishedAt など既存スナップショットから推定する軽量ビュー。 export function buildSourceLineage(params: { sources: SourceForLineage[]; evidenceClaims: RunEvidenceClaim[]; diff --git a/src/features/run/lib/build-unknowns.ts b/src/features/run/lib/build-unknowns.ts index 18f0142..c5b7ed1 100644 --- a/src/features/run/lib/build-unknowns.ts +++ b/src/features/run/lib/build-unknowns.ts @@ -15,6 +15,8 @@ function fromAlert(params: { id: string; message: string; level: AlertLevel; cla return { id: params.id, relatedClaimIds: params.claimSummary ? [params.id.split("-")[0]] : [], relatedSourceIds: [], text: params.claimSummary ? `${params.claimSummary}: ${params.message}` : params.message, reason: m.includes("primary source") ? "Primary or official evidence is missing." : "Existing evidence raised an investigation caveat.", severity: severityFromAlertLevel(params.level), category, suggestedNextAction: next, signals: ["alert"] }; } +// Unknown Mapは専用テーブルではなく、 +// claim/alert/source-qualityから派生する調査上の「未解決点ビュー」。 export function buildUnknowns(params: { evidenceAlerts: RunEvidenceAlert[]; evidenceClaims: RunEvidenceClaim[]; sourceQuality?: SourceQualitySignal[]; sourceDrilldown?: SourceDetailDrilldown[] }): InvestigationUnknown[] { const unknowns: InvestigationUnknown[] = []; params.evidenceAlerts.forEach((a) => unknowns.push(fromAlert(a))); @@ -40,6 +42,8 @@ export function buildUnknowns(params: { evidenceAlerts: RunEvidenceAlert[]; evid }); }); + // 同一論点の重複表示を避けるため、category+関連ID+reasonで集約し、 + // severityが高いものを優先して残す。 const deduped = new Map(); for (const u of unknowns) { const key = `${u.category}|${(u.relatedClaimIds ?? []).sort().join(",")}|${(u.relatedSourceIds ?? []).sort().join(",")}|${u.reason.toLowerCase()}`; diff --git a/src/server/analysis/create-analysis-run-from-provider.ts b/src/server/analysis/create-analysis-run-from-provider.ts index 9d83c04..dcbab1b 100644 --- a/src/server/analysis/create-analysis-run-from-provider.ts +++ b/src/server/analysis/create-analysis-run-from-provider.ts @@ -36,6 +36,8 @@ export async function createAnalysisRunFromProvider( options.mode ?? process.env.TRACEMAP_INVESTIGATION_MODE?.trim(), ); + // 手動URL指定時はユーザーが明示した調査条件を優先し、 + // 既存run cacheの再利用で入力意図が薄まることを避ける。 const hasManualSourceUrls = (options.manualSourceUrls?.length ?? 0) > 0; const run = await prisma.analysisRun.create({ @@ -46,6 +48,8 @@ export async function createAnalysisRunFromProvider( }, }); + // MVP v2 は同期実行のため、run status は queued -> processing -> completed/failed を + // この関数内で完結させる。 await prisma.analysisRun.update({ where: { id: run.id }, data: { status: "processing" }, @@ -61,6 +65,8 @@ export async function createAnalysisRunFromProvider( let payload: GeneratedAnswerGraphPayload | null = null; let shouldStoreRunCache = false; + // run cacheは「同一topic + provider + mode」でのみ再利用する。 + // 手動URL付きrunは上書き条件なのでlookup自体を行わない。 if (!hasManualSourceUrls) { try { const cached = await lookupRunCacheEntry(cacheKeyInfo); @@ -100,6 +106,7 @@ export async function createAnalysisRunFromProvider( result = await provider.generateAnswerGraph({ question, sourceCandidates: sourceIntake.candidates, mode }); } catch (cause) { console.error("[analysis] generateAnswerGraph threw", { runId: run.id, cause }); + // provider例外は生メッセージをUIへ出さず、安全な固定文言へ変換して保存する。 await prisma.analysisRun.update({ where: { id: run.id }, data: { diff --git a/src/server/analysis/persist-generated-answer-graph.ts b/src/server/analysis/persist-generated-answer-graph.ts index dde59ee..1d57272 100644 --- a/src/server/analysis/persist-generated-answer-graph.ts +++ b/src/server/analysis/persist-generated-answer-graph.ts @@ -105,6 +105,8 @@ export async function persistGeneratedAnswerGraph(params: { }): Promise { const { runId, payload } = params; + // source検証はbest-effort。到達不能でもrun全体は継続し、 + // Unknown/Source Quality側で「注意情報」として表示できる形に寄せる。 const verificationByIndex = await Promise.all( payload.sources.map((src) => shouldResolveSourceCache(src.url) @@ -150,6 +152,8 @@ export async function persistGeneratedAnswerGraph(params: { idMap.set(placeholderId, row.id); } + // providerは仮ID(__src_n__)を返すため、永続化後のsourceSnapshotIdへ張り替えて + // graph snapshotをDB参照可能な形に正規化する。 const graph = replaceGraphSourceIds(payload.answer.graphJson, idMap); await tx.answerSnapshot.update({ @@ -170,6 +174,7 @@ export async function persistGeneratedAnswerGraph(params: { }, }); + // claim-source参照はsupports優先。legacy配列は後方互換としてdirect supportへ変換。 const supportRelations = c.supports ? c.supports.flatMap((support) => { const sourceSnapshotId = idMap.get(support.sourcePlaceholderId); diff --git a/src/server/analysis/source-intake/source-intake-service.ts b/src/server/analysis/source-intake/source-intake-service.ts index 6fcc125..590c572 100644 --- a/src/server/analysis/source-intake/source-intake-service.ts +++ b/src/server/analysis/source-intake/source-intake-service.ts @@ -20,6 +20,7 @@ export async function buildSourceIntakeFromQuestion( const discoveryProvider = resolveSourceDiscoveryProvider(); const ignoredUrls: SourceIntakeResult["ignoredUrls"] = []; + // source discoveryは補助入力。失敗時もrunを止めず、ignoredUrlsへ理由を残す。 let discoveredUrls: string[] = []; if (discoveryProvider.id !== "disabled") { const discovery = await discoveryProvider.discoverSources({ @@ -33,6 +34,8 @@ export async function buildSourceIntakeFromQuestion( } } + // manual > topic中URL > discovery候補を同一パイプラインへ流し、 + // URL正規化/安全判定/重複排除を一箇所で適用する。 const merged = [ ...manualUrls.map((url) => ({ url, origin: "manual_url" as const })), ...topicUrls.map((url) => ({ url, origin: "topic_url" as const })), @@ -57,6 +60,7 @@ export async function buildSourceIntakeFromQuestion( ignoredUrls.push({ url: item.url, reason: result.errorMessage }); continue; } + // 最終URLではなくnormalized URLでdedupeし、同一記事の追跡パラメータ差分を吸収する。 if (seen.has(result.normalizedUrl)) continue; seen.add(result.normalizedUrl); diff --git a/src/types/answer-graph-generation.ts b/src/types/answer-graph-generation.ts index 0735329..cc8253a 100644 --- a/src/types/answer-graph-generation.ts +++ b/src/types/answer-graph-generation.ts @@ -101,6 +101,10 @@ export type GeneratedEvidenceBundle = { }; /** Persistable result of a successful generation step (DB writer consumes this). */ +/** + * provider出力を永続化用へ正規化した中間payload。 + * graphJsonは描画スナップショットで、claim/source実体は別テーブルへ保存される。 + */ export type GeneratedAnswerGraphPayload = { answer: { title: string | null; diff --git a/tests/source-intake.test.ts b/tests/source-intake.test.ts index 520ee5d..0928cdd 100644 --- a/tests/source-intake.test.ts +++ b/tests/source-intake.test.ts @@ -4,6 +4,7 @@ import { extractUrls } from "@/server/analysis/source-intake/extract-urls"; import { normalizeSourceUrl } from "@/server/analysis/source-url-normalization"; import { isSourceFetchSafe } from "@/server/analysis/source-fetch-safety"; +// Source intakeは「抽出→正規化→安全判定」の入口仕様を守るテスト。 describe("source intake basics", () => { it("extracts multiple http/https URLs and trims trailing punctuation", () => { const result = extractUrls("See https://Example.com/a, and http://example.org/b?x=1.");