From 2725d1abf84f6008499e9f867640b5b8615b5799 Mon Sep 17 00:00:00 2001 From: Theater-ahyeon <1347507191@qq.com> Date: Mon, 21 Sep 2026 20:50:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(research):=20=E5=A2=9E=E5=8A=A0=E9=92=88?= =?UTF-8?q?=E5=AF=B9=E7=A0=94=E6=8A=A5=E5=85=A8=E9=83=A8=20Claims=20?= =?UTF-8?q?=E7=9A=84=E7=AB=AF=E5=88=B0=E7=AB=AF=E9=AA=8C=E8=AF=81=E8=81=9A?= =?UTF-8?q?=E5=90=88=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/research/claim-verifier.test.ts | 31 +++++++++- .../shared/src/research/claim-verifier.ts | 58 +++++++++++++++++++ packages/shared/src/research/index.ts | 11 ++++ 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/research/claim-verifier.test.ts b/packages/shared/src/research/claim-verifier.test.ts index 4598a138..100c5ff7 100644 --- a/packages/shared/src/research/claim-verifier.test.ts +++ b/packages/shared/src/research/claim-verifier.test.ts @@ -3,9 +3,9 @@ import type { JudgeClient } from '../evaluation/judge-client.ts'; import { CLAIM_VERIFIER_VERSION, createClaimVerifier, + verifyReportClaims, type ClaimVerificationInput, } from './claim-verifier.ts'; - function input( claimText: string, evidence: ClaimVerificationInput['evidence'], @@ -143,4 +143,33 @@ describe('claim verifier', () => { expect(result.evidenceIds).toEqual(['source-evidence']); expect(result.evidenceIds).not.toContain('Explanation only.'); }); + + it('verifies all claims across research report sections end to end', async () => { + const judge = judgeReturning('{"status":"supported","reason":"Confirmed by filing excerpt."}'); + const verifier = createClaimVerifier(judge); + const report = { + id: 'report-123', + sections: [ + { + evidence: [ + { claim: 'Revenue rose 10%', runId: 'sec-edgar-run-1', summary: 'SEC 10-Q filing notes 10% revenue increase' }, + { claim: 'Gross margin expanded', runId: 'run-2', summary: 'Margin up 150bps' }, + ], + }, + { + evidence: [ + { claim: 'Revenue rose 10%', runId: 'sec-edgar-run-1', summary: 'Same claim in conclusion' }, + ], + }, + ], + }; + const summary = await verifyReportClaims(report, verifier); + + expect(summary.reportId).toBe('report-123'); + expect(summary.totalClaims).toBe(2); // Deduplicated identical claim statement + expect(summary.supported).toBe(2); + expect(summary.contradicted).toBe(0); + expect(summary.insufficientEvidence).toBe(0); + expect(summary.results).toHaveLength(2); + }); }); diff --git a/packages/shared/src/research/claim-verifier.ts b/packages/shared/src/research/claim-verifier.ts index eb2afc74..f6ce8679 100644 --- a/packages/shared/src/research/claim-verifier.ts +++ b/packages/shared/src/research/claim-verifier.ts @@ -32,6 +32,64 @@ export interface ClaimVerifier { verify(input: ClaimVerificationInput, signal?: AbortSignal): Promise; } +export interface ReportClaimVerificationSummary { + reportId: string; + verifiedAt: number; + totalClaims: number; + supported: number; + contradicted: number; + insufficientEvidence: number; + results: ClaimVerificationResult[]; +} + +/** + * Verify all atomic claims within a ResearchReport against their associated evidence. + * Extracts EvidenceRef claims across all sections and checks each using the ClaimVerifier. + */ +export async function verifyReportClaims( + report: { id: string; sections?: Array<{ evidence?: Array<{ claim: string; runId?: string; summary?: string }> }> }, + verifier: ClaimVerifier, + signal?: AbortSignal +): Promise { + const seenClaims = new Map(); + for (const section of report.sections ?? []) { + for (const ref of section.evidence ?? []) { + if (!ref.claim) continue; + const existing = seenClaims.get(ref.claim) ?? []; + if (ref.summary) { + existing.push({ id: ref.runId ?? `evidence-${existing.length + 1}`, content: ref.summary }); + } + seenClaims.set(ref.claim, existing); + } + } + + const results: ClaimVerificationResult[] = []; + let supported = 0; + let contradicted = 0; + let insufficientEvidence = 0; + + let idx = 0; + for (const [claimText, evidenceList] of seenClaims.entries()) { + idx += 1; + const claimId = `claim-${idx}`; + const res = await verifier.verify({ claimId, claimText, evidence: evidenceList }, signal); + results.push(res); + if (res.status === 'supported') supported += 1; + else if (res.status === 'contradicted') contradicted += 1; + else insufficientEvidence += 1; + } + + return { + reportId: report.id, + verifiedAt: Date.now(), + totalClaims: results.length, + supported, + contradicted, + insufficientEvidence, + results, + }; +} + interface JudgeResult { status: ClaimVerificationStatus; reason: string; diff --git a/packages/shared/src/research/index.ts b/packages/shared/src/research/index.ts index 2b071548..15359e40 100644 --- a/packages/shared/src/research/index.ts +++ b/packages/shared/src/research/index.ts @@ -21,3 +21,14 @@ export { type SanitizePolicy, type SanitizeResult, } from './sanitize.ts'; +export { + CLAIM_VERIFIER_VERSION, + createClaimVerifier, + verifyReportClaims, + type ClaimVerificationInput, + type ClaimVerificationResult, + type ClaimVerificationStatus, + type ClaimVerifier, + type ClaimVerifierEvidence, + type ReportClaimVerificationSummary, +} from './claim-verifier.ts';