(
answerText: string,
blockRefs: BlockRefSpec[],
opts: { useJudge?: boolean } = {},
)
| 195 | } |
| 196 | |
| 197 | export async function verifyBlockCitations( |
| 198 | answerText: string, |
| 199 | blockRefs: BlockRefSpec[], |
| 200 | opts: { useJudge?: boolean } = {}, |
| 201 | ): Promise<CitationVerifyResult> { |
| 202 | const useJudge = opts.useJudge ?? judgeAvailable(); |
| 203 | const brokenAnchors: string[] = []; |
| 204 | const downgraded: string[] = []; |
| 205 | const unverified: string[] = []; |
| 206 | |
| 207 | const byKey = new Map<string, BlockRefSpec>(); |
| 208 | for (const r of blockRefs) if (!byKey.has(r.key)) byKey.set(r.key, r); |
| 209 | const uniq = [...byKey.values()]; |
| 210 | |
| 211 | const reads = await Promise.all( |
| 212 | uniq.map((r) => |
| 213 | executeTool("read_block", { path: r.path, anchor: `^${r.anchor}` }).then((res) => ({ r, res })), |
| 214 | ), |
| 215 | ); |
| 216 | |
| 217 | const grays: Array<{ key: string; claim: string; content: string }> = []; |
| 218 | |
| 219 | for (const { r, res } of reads) { |
| 220 | if (!res.ok) { |
| 221 | if (errorIsNotFound(res.error || "")) { |
| 222 | brokenAnchors.push(r.key); |
| 223 | downgraded.push(r.key); |
| 224 | } else { |
| 225 | unverified.push(r.key); // 网络/超时 → 不降级 |
| 226 | } |
| 227 | continue; |
| 228 | } |
| 229 | const content = blockContent(res.data); |
| 230 | if (!content.trim()) { |
| 231 | unverified.push(r.key); |
| 232 | continue; |
| 233 | } |
| 234 | if (blockKind(res.data) === "heading") continue; // heading 指整节、只回标题行,豁免 |
| 235 | |
| 236 | const ctxs = claimContextsFor(answerText, r.key).filter((c) => hasSalient(c)); |
| 237 | if (ctxs.length === 0) continue; |
| 238 | |
| 239 | const blockTok = hardNumericTokens(content); |
| 240 | let hardSupported = false; // 某出现处被块的 strong 要素命中 → 整 key 保留 |
| 241 | let numericMismatch = false; // 某出现处有 strong 要素、块全未命中 |
| 242 | let needsJudge = false; // 纯定性 / 仅年份 → 灰区 |
| 243 | let judgeClaim = ""; |
| 244 | for (const ctx of ctxs) { |
| 245 | const ct = hardNumericTokens(ctx); |
| 246 | if (ct.strong.size >= 1) { |
| 247 | if (matchHits(ct, blockTok).strongHit >= 1) hardSupported = true; |
| 248 | else numericMismatch = true; |
| 249 | } else { |
| 250 | // 仅年份 或 无硬要素 → 不凭启发式定,交灰区 |
| 251 | needsJudge = true; |
| 252 | if (!judgeClaim) judgeClaim = ctx; |
| 253 | } |
| 254 | } |
no test coverage detected