Kaynağa Gözat

feat(research67): 展示科研执行范围与结果复核证据

分别呈现研究计划、实际计算、观察解释、数值核对和分项验收,避免将执行结束视为科学结论成立。同步科研智能体说明及工具结果,增加 CLI 卡片和工具格式回归覆盖。
邵勃源 11 saat önce
ebeveyn
işleme
6d7e0e2533

+ 12 - 0
.opencode/README.agentpaas.md

@@ -51,6 +51,18 @@ export AGENTPAAS_API_KEY=你的租户APIKey
 
 ## 运行状态与断线
 
+科研任务可返回 `research.science.research_ir`。终端同时显示科研状态和 IR 的执行范围:
+`plans_only` 为仅规划,`blocked` 为暂不能执行,`executed` 表示已登记的实验子程序执行完成,
+`reused_observations` 不计为新实验。高层行动数量不等于已执行数量。
+通过原任务的产物 API 下载 `execution-ir.json` 和对应 `cek-receipt.json` 步骤凭据;本地与远端使用相同方式。
+科研卡片同时显示下一步研究,以及观察记录、作者解释和模型审阅认可的数量。
+启用科研配置 `synthesis_enabled: true` 后,结果综合会逐项给出限定范围的回答、实验引用、限制和下一步,产物为 `research-synthesis.json`。卡片单独展示综合解释的复核状态;这不代表新增实验或独立科学验收,也不自动消除文献缺口。
+
+数值核对卡片同时显示原文数字片段的关联情况;未关联项的原句、路径和位置保存在 `numeric_check.artifact` 指向的检查记录中。计划值也可能被列出,因此未关联不直接判错;该计数不是科学结论正确率或语义核验覆盖率。
+后台可对遗漏位置做一次有界补查,卡片展示恢复关联、模型判断非结果和无法对应的数量。补查未完成不会清空首次检查;新关联仍须通过相同的引用和数值检查,原有错误不会被覆盖。
+`observation-ledger.json` 将原始观察与模型解释分开保存;`research-decisions.json` 提供带适用条件、检索词和影响路径的证据任务。
+只复核已有实验时可明确说“使用 reuse_observations,不重新执行或改变参数”。服务端核对已有记录、执行器版本及参数;缺少原观察或参数不一致会返回修订问题,不启动替代实验。
+
 `completed` 表示执行结束;`partial` 是部分完成;`needs_clarification` 表示需要补充信息;`paused` 和等待审批均不等于失败。科研状态另行保留,模拟已执行也不代表现实结论已验证。
 
 `status` 独立读取运行快照;`watch` 的事件接口故障不会丢掉已取得的结果和产物,会返回明确提示。每次普通工具确认也有独立 `approval_id`,迟到、重复或缺少编号的决定被拒绝。两端与执行服务需配套更新;旧服务没有确认编号时,客户端不会批准未绑定的操作。

+ 22 - 1
.opencode/agent/research67.md

@@ -29,7 +29,17 @@ tools:
 不要在失败后自动重跑、换键、拆分请求或改调其他 Agent。保留原 run_id 和已生成的产物。
 
 参赛配置的交付物是候选假设、来源证据、研究计划、程序检查和反馈修订。
-明确启用时可执行 Q124 受限高斯模拟,其他计划中的动作默认没有执行。
+明确配置并通过准入时可执行服务端已登记的实验适配器;以本次能力与执行凭据为准,不按题号推断能力。
+有 research.science.research_ir 时,说明 IR 状态、本轮实际执行的实验子程序和高层计划行动数。
+Lambda/CEK 的类型检查及运行步骤只描述执行程序,不能证明科学假设正确;高层行动不能因一个子程序完成而全部标为完成。
+查看细节时按产物 API 获取 execution-ir.json 及对应的 cek-receipt 凭据,不读取远端服务器路径。
+有 observations 时分别说明已有观察、作者解释和模型审阅的数量;原始观察与解释见 observation-ledger.json。
+有 science.synthesis 时,优先通过产物 API 获取 research-synthesis.json,按子问题说明回答、实验编号、适用边界和下一步。
+它是对已有结果的综合解释,不能当成新实验;reviewed 表示模型复核通过,不改变 needs_evidence 等研究状态。
+一个综合判断可引用多个前置实验;不能因为它没有新增计算,就说整个子问题尚未分析。只转述后端明确给出的 finding。
+仅查看结果或下载总结不启动新任务,不要求用户再次运行同一实验。
+优先解释 science.next_step,按证据任务补充材料或修订原研究,不把实验记录存在说成反馈已解决。
+用户只要求复核已有结果时,应明确保留原参数并使用 reuse_observations;不能用重新提交同一模拟代替复核。
 是否接入其他流程或仪器,必须以远端实际返回为准;不要自行介绍论文写作、实验授权或其他未确认能力。
 
 status=completed 只表示执行结束。若有 research.science.status,优先原样解释:
@@ -55,3 +65,14 @@ status=completed 只表示执行结束。若有 research.science.status,优先
 客户端辅助操作:查询能力用 agentpaas_agents;用户材料用 agentpaas_upload 上传并将 file_id 传给 agentpaas_run.input_file_ids;进度/暂停/恢复/审批用 agentpaas_task 操作原 run_id;明确选择历史任务继续修订时,先用 agentpaas_task 的 select 绑定该 run_id 的会话与父版本,单纯查询不切换上下文;取回产物用 agentpaas_download。辅助操作不受“每回合只执行一次 agentpaas_run”的限制。不要把查询、下载或恢复变成新的科研任务,不把服务端路径当成本机路径。只有用户明确要求新一轮修订时才再次运行。
 
 长任务提交使用 agentpaas_run.background=true,受理后向用户报告任务编号与真实状态,不在同一回合无限轮询。重新打开或不知道原编号时用 agentpaas_tasks 找回;用 task/watch 观察、cancel 停止。后台任务等待确认时需用户明确决定,不能自行批准。每次新提交会自动检查已知资源故障;要验证模型或知识检索,用 agentpaas_check(check_model 会调用模型)。
+
+后端返回 resources 时,说明可用方法操作及具体缺口;已注册执行器不表示能验证所有同类问题。
+后端返回 acceptance 时,区分方案检查、计算凭据、结果解释、方法资源、独立科学评价与远端验收。
+缺测项保留“未开展/尚未验收”;不把结果综合的模型复核称为专家验证,不自行计算科学总分。
+
+resources.datasets 中的 profile 是对注册输入的确定性概况,用于核对字段、分组和缺失情况,不算实际实验执行。
+预览 values_complete/states_complete 为 false 时,未显示值不代表不存在;执行结果以 IR、观察凭据和原始结果为准。
+
+结果综合中的 numeric_check 只核对模型已提取且绑定结果字段的数值。显示提取数量与核对状态;0 条不能解释为“所有数字正确”,checked 也不等于完整语义或独立科学验证。
+若 numeric_check.coverage 存在,同时说明候选数字片段、已关联和未关联数量。未关联项可能是计划值、标识或漏提的结果数字,不直接判错;需要解释时读取 numeric_check.artifact 中的 quantity_coverage.unlinked,按原文位置核对。所有片段均关联也不代表语义绑定无误。综合结论和审阅意见使用该研究项 observation_ids 中的实验,不能从其他实验挑选相同数值作为依据。
+numeric_check.followup 记录一次有界遗漏补查。分别说明恢复关联、模型判断非结果及暂无法对应的数量;分类理由保存在其 artifact 中。not_result_mentions 是模型分类,不是程序证明不需核验;unresolved 不能当作正确。补查 pending 时保留首次结果,提醒查看未关联项,不自动重跑整个实验。

+ 13 - 0
.opencode/lib/agentpaas.ts

@@ -392,6 +392,12 @@ export function formatRunResult(agentName: string, run: Record<string, unknown>,
   const artifacts = Array.isArray(run.artifacts) ? run.artifacts : []
   const metadata: Record<string, unknown> = { ...run, ...extra, status, artifacts }
   const science = asRecord(asRecord(run.research)?.science)
+  const researchIR = asRecord(science?.research_ir)
+  const observations = asRecord(science?.observations)
+  const irLabels: Record<string, string> = {
+    not_compiled: "尚未编译", plans_only: "仅规划", blocked: "暂不能执行", ready: "可执行",
+    executed: "实验子程序已执行", reused_observations: "复用观察", failed: "执行失败", cancelled: "已取消",
+  }
   const hints: Record<string, string> = {
     partial: "任务已部分完成,保留已有产物;请查看缺失项后继续补充。",
     needs_clarification: "需要补充信息后才能继续;请回答智能体提出的问题,不要重新执行同一请求。",
@@ -418,6 +424,13 @@ export function formatRunResult(agentName: string, run: Record<string, unknown>,
         : `AgentPaaS ${agentName}(${labels[status] ?? "状态待核查"})`,
     output: [
       ...(science ? [`研究状态:${scientificLabel(science.status)}。执行结束不代表科学结论成立。`] : []),
+      ...(getString(science?.next_step) ? [`下一步研究:${getString(science?.next_step)}`] : []),
+      ...(observations ? [`观察记录:${observations.recorded ?? "未报告"} 份;已解释 ${observations.interpreted ?? "未报告"} 份;模型审阅认可 ${observations.reviewed ?? "未报告"} 份。详情见 observation-ledger.json。`] : []),
+      ...(researchIR ? [
+        `科研 IR:${irLabels[getString(researchIR.status)] || "状态待核查"};本轮已执行 ${researchIR.executed_kernels ?? "未报告"} 个实验子程序。`,
+        `高层研究计划含 ${researchIR.planned_actions ?? "未报告"} 项行动,不代表全部已执行;详情见 execution-ir.json。`,
+        ...(researchIR.engine === "lambda_cek" ? ["执行引擎:Lambda / CEK;程序凭据不代表科学结论成立。"] : []),
+      ] : []),
       ...(science?.execution_status === "conditional_diagnostic_completed"
         ? ["模拟状态:已完成设定模型下的诊断;证据充分性与现实适用性须另行判断。"]
         : []),

+ 38 - 0
packages/opencode/test/tool/agentpaas-research-ir.test.ts

@@ -0,0 +1,38 @@
+import { expect, test } from "bun:test"
+import { formatRunResult } from "../../../../.opencode/lib/agentpaas"
+
+test("research result separates recorded observations from interpretation and review", () => {
+  const result = formatRunResult("Research67", { status: "completed", research: { science: {
+    status: "needs_evidence", next_step: "补充模型适用性证据", observations: { recorded: 2, interpreted: 1, reviewed: 0 },
+  } } })
+  expect(result.output).toContain("下一步研究:补充模型适用性证据")
+  expect(result.output).toContain("观察记录:2 份;已解释 1 份;模型审阅认可 0 份")
+})
+
+for (const [status, label, count] of [
+  ["plans_only", "仅规划", 0],
+  ["blocked", "暂不能执行", 0],
+  ["executed", "实验子程序已执行", 1],
+  ["reused_observations", "复用观察", 0],
+] as const) {
+  test(`research IR ${status} never marks all planned actions complete`, () => {
+    const result = formatRunResult("Research67", {
+      status: "completed",
+      research: { science: { status: "needs_evidence", research_ir: {
+        status, planned_actions: 5, executed_kernels: count,
+        engine: status === "executed" ? "lambda_cek" : null,
+      } } },
+    })
+    expect(result.title).toBe("证据不足")
+    expect(result.output).toContain(`科研 IR:${label}`)
+    expect(result.output).toContain(`本轮已执行 ${count} 个实验子程序`)
+    expect(result.output).toContain("5 项行动,不代表全部已执行")
+    expect(result.output).toContain("execution-ir.json")
+  })
+}
+
+test("legacy research result does not invent an IR or execution engine", () => {
+  const result = formatRunResult("Research67", { status: "completed", research: { science: { status: "needs_review" } } })
+  expect(result.output).not.toContain("科研 IR:")
+  expect(result.output).not.toContain("Lambda / CEK")
+})

+ 62 - 0
packages/tui/src/util/agentpaas.ts

@@ -35,6 +35,22 @@ export function paasCard(metadata: Record<string, unknown>, input: Record<string
     unknown: "用原 run_id 查询;不要重新提交",
   }
   const science = record(record(metadata.research)?.science)
+  const operation = record(science?.operation)
+  const researchIR = record(science?.research_ir)
+  const observations = record(science?.observations)
+  const control = record(science?.control)
+  const capabilities = record(science?.capabilities)
+  const schedule = record(science?.study_schedule)
+  const synthesis = record(science?.synthesis)
+  const numericCheck = record(synthesis?.numeric_check)
+  const numericCoverage = record(numericCheck?.coverage)
+  const numericFollowup = record(numericCheck?.followup)
+  const resources = record(science?.resources)
+  const acceptance = record(record(science?.acceptance)?.dimensions)
+  const irLabels: Record<string, string> = {
+    not_compiled: "尚未编译", plans_only: "仅规划", blocked: "暂不能执行", ready: "可执行",
+    not_executed: "本轮未执行", executed: "实验子程序已执行", reused_observations: "复用观察", failed: "执行失败", cancelled: "已取消",
+  }
   const usage = record(metadata.usage)
   const artifacts = Array.isArray(metadata.artifacts)
     ? metadata.artifacts.flatMap((item) => {
@@ -59,6 +75,52 @@ export function paasCard(metadata: Record<string, unknown>, input: Record<string
           })
         : []),
       science ? `研究状态:${text(science.status) || "未报告"};执行结束不代表科学结论成立` : "",
+      operation?.operation === "review_results" ? "本次操作:复核已有结果;保留研究定义与实验记录,未重新规划或执行实验" : "",
+      operation?.operation === "clarify" ? "本次操作:等待说明研究意图;未启动研究或实验" : "",
+      record(acceptance?.plan_review)?.status === "not_reassessed" ? "原方案:本次未重审,原有待修订项继续保留;review-parent-gate.json" : "",
+      text(science?.next_step) ? `下一步研究:${text(science?.next_step)}` : "",
+      control?.continue_automatically === false ? "本次已停止;上述后续建议尚未自动执行,可在当前会话继续补充条件或反馈" : "",
+      capabilities?.status === "plans_only_no_registered_input" ? "当前可生成研究计划;实际计算需先注册数据或选择模拟器" : "",
+      capabilities?.status === "selection_required" ? "已登记多类数据,请选择本项研究要使用的执行器" : "",
+      capabilities?.status === "per_round_selection" ? "按研究任务逐轮选择已注册执行器;下游数据保留来源" : "",
+      schedule ? `研究任务:${number(schedule.computed)}/${number(schedule.total)} 项计算完成(不等于科学结论成立)` : "",
+      ...(Array.isArray(schedule?.units) ? schedule.units.flatMap((item) => {
+        const row = record(item)
+        if (!row) return []
+        const state = row.interpretation_status === "reviewed"
+          ? ({ answered_within_scope: "已形成限定范围回答", inconclusive: "结果尚不足以判断", not_evaluated: "尚未验证" } as Record<string, string>)[text(row.finding)] || "解释已复核"
+          : ({ computed: "计算已完成,待解释", ready_to_plan: "待规划", waiting_prerequisite: "等待前置计算" } as Record<string, string>)[text(row.status)] || "状态待核查"
+        const parents = Array.isArray(row.depends_on) ? row.depends_on.map(text).filter(Boolean).join("、") : ""
+        return [`${text(row.study_id)} · ${state}${parents ? `;依赖 ${parents}` : ""}`]
+      }) : []),
+      synthesis ? `结果综合:${({ reviewed: "模型复核通过", needs_revision: "解释待修订", review_pending: "待复核", uncertain: "存在不确定项" } as Record<string, string>)[text(synthesis.status)] || "状态待核查"} · ${number(synthesis.answered)}/${number(synthesis.total)} 项限定范围回答;不是独立科学验收;research-synthesis.json` : "",
+      numericCheck ? `数值核对:${({ checked: "已核对提取项", contradictions: "发现不一致", pending: "尚未完成" } as Record<string, string>)[text(numericCheck.status)] || "状态待核查"} · 提取 ${number(numericCheck.quantity_claims)} 条;不代表已覆盖全部表述` : "",
+      numericCoverage ? `数字片段:${number(numericCoverage.linked_mentions)}/${number(numericCoverage.candidate_mentions)} 已关联;${number(numericCoverage.unlinked_mentions)} 未关联(可能包含计划值或标识);详见 ${text(numericCheck?.artifact) || "数值核对记录"},不是语义核验覆盖率` : "",
+      numericFollowup ? numericFollowup.status === "completed"
+        ? `遗漏补查:恢复 ${number(numericFollowup.recovered_mentions)} 处 · 模型判断非结果 ${number(numericFollowup.not_result_mentions)} 处 · 暂无法对应 ${number(numericFollowup.unresolved_mentions)} 处;${text(numericFollowup.artifact)},分类仍需语义核对`
+        : "遗漏补查尚未完成;保留首次核对与未关联项,不表示全文通过" : "",
+      ...(Array.isArray(resources?.methods) ? resources.methods.flatMap((item) => {
+        const row = record(item)
+        if (!row) return []
+        const executors = Array.isArray(row.executors) ? row.executors.map(text).filter(Boolean).join("、") : ""
+        return [`方法资源:${text(row.method)} · ${row.status === "no_registered_operation" ? "缺少执行适配器,可先完善研究计划" : `可用操作 ${executors || "查看资源报告"},需核对适用范围`}`]
+      }) : []),
+      ...(Array.isArray(resources?.datasets) ? resources.datasets.flatMap((item) => {
+        const data = record(item)
+        const profile = record(data?.profile)
+        if (!data || !profile) return []
+        const size = profile.kind === "table"
+          ? `${number(profile.row_count)} 行 · ${number(profile.malformed_rows)} 行结构异常`
+          : profile.kind === "finite_graph" ? `${number(profile.state_count)} 个状态 · ${number(profile.transition_count)} 条转移` : "概况待核查"
+        return [`输入概况:${text(data.id)} · ${size};不是实验结果`]
+      }) : []),
+      acceptance ? "分项验收:research-acceptance.json;方案检查、计算和结果解释分别记录,不折算科学总分" : "",
+      record(acceptance?.independent_scientific_evaluation)?.status === "not_run" ? "独立科学评价:未开展" : "",
+      record(acceptance?.remote_or_instrument_acceptance)?.status === "not_verified" ? "远端或仪器验收:尚未完成" : "",
+      observations ? `观察 ${number(observations.recorded)} 份 · 已解释 ${number(observations.interpreted)} 份 · 模型审阅认可 ${number(observations.reviewed)} 份;observation-ledger.json` : "",
+      researchIR ? `科研 IR:${irLabels[text(researchIR.status)] || "状态待核查"} · 本轮执行 ${number(researchIR.executed_kernels)} 个实验子程序` : "",
+      researchIR && !["review_results", "clarify"].includes(text(operation?.operation)) ? `研究计划 ${number(researchIR.planned_actions)} 项行动,不代表全部已执行;execution-ir.json 查看范围` : "",
+      researchIR?.engine === "lambda_cek" ? "执行引擎:Lambda / CEK;可下载步骤凭据" : "",
       record(metadata.research)?.revision !== undefined ? `研究版本:${record(metadata.research)?.revision}` : "",
       `Tokens: ${number(metadata.total_tokens ?? usage?.total_tokens)} · USD: ${number(metadata.cost_usd ?? usage?.cost_usd)}`,
       `计量:tokens ${usage?.usage_complete === true ? "完整" : "未确认完整"};费用 ${usage?.cost_complete === true ? "完整" : "未确认完整,0 不代表免费"}`,

+ 129 - 0
packages/tui/test/util/agentpaas.test.ts

@@ -1,6 +1,66 @@
 import { expect, test } from "bun:test"
 import { paasCard } from "../../src/util/agentpaas"
 
+test("reviewed synthesis is distinguished from computation and scientific acceptance", () => {
+  const card = paasCard({ status: "completed", research: { science: {
+    status: "needs_evidence", synthesis: { status: "reviewed", answered: 1, total: 2 },
+    study_schedule: { computed: 1, total: 2, units: [
+      { study_id: "T1", status: "computed", interpretation_status: "reviewed", finding: "answered_within_scope", depends_on: [] },
+      { study_id: "T2", status: "ready_to_plan", interpretation_status: "reviewed", finding: "inconclusive", depends_on: ["T1"] },
+    ] },
+  } } })
+  const lines = card.lines.join("\n")
+  expect(lines).toContain("T1 · 已形成限定范围回答")
+  expect(lines).toContain("T2 · 结果尚不足以判断")
+  expect(lines).toContain("1/2 项限定范围回答;不是独立科学验收")
+  expect(lines).toContain("needs_evidence")
+  expect(lines).not.toContain("待解释")
+})
+
+test("study schedule shows dependency readiness separately from scientific success", () => {
+  const card = paasCard({ status: "completed", research: { science: {
+    capabilities: { status: "per_round_selection" },
+    study_schedule: { computed: 1, total: 3, units: [
+      { study_id: "T1", status: "computed", depends_on: [] },
+      { study_id: "T2", status: "ready_to_plan", depends_on: ["T1"] },
+      { study_id: "T3", status: "waiting_prerequisite", depends_on: ["T2"] },
+    ] },
+  } } })
+  expect(card.lines.join("\n")).toContain("1/3 项计算完成(不等于科学结论成立)")
+  expect(card.lines.join("\n")).toContain("T2 · 待规划;依赖 T1")
+  expect(card.lines.join("\n")).toContain("T3 · 等待前置计算;依赖 T2")
+  expect(card.lines.join("\n")).toContain("逐轮选择已注册执行器")
+})
+
+test("stopped research recommendations are not displayed as automatically running", () => {
+  const card = paasCard({ status: "completed", research: { science: {
+    next_step: "补充定义后继续推导", control: { continue_automatically: false },
+    capabilities: { status: "plans_only_no_registered_input" },
+  } } })
+  expect(card.lines.join("\n")).toContain("上述后续建议尚未自动执行")
+  expect(card.lines.join("\n")).toContain("实际计算需先注册数据")
+})
+
+test("research card displays next research action and observation review separately", () => {
+  const card = paasCard({ status: "completed", research: { science: {
+    status: "needs_evidence", next_step: "补充模型适用性证据", observations: { recorded: 2, interpreted: 1, reviewed: 0 },
+  } } })
+  expect(card.lines.join("\n")).toContain("下一步研究:补充模型适用性证据")
+  expect(card.lines.join("\n")).toContain("观察 2 份 · 已解释 1 份 · 模型审阅认可 0 份")
+})
+
+test("scientific card separates planned actions, executed kernels and reused observations", () => {
+  for (const status of ["plans_only", "blocked", "reused_observations", "executed"]) {
+    const card = paasCard({ status: "completed", research: { science: {
+      status: "needs_evidence", research_ir: { status, planned_actions: 7,
+        executed_kernels: status === "executed" ? 1 : 0, engine: status === "executed" ? "lambda_cek" : null },
+    } } })
+    expect(card.lines.join("\n")).toContain("needs_evidence")
+    expect(card.lines.join("\n")).toContain("研究计划 7 项行动,不代表全部已执行")
+    expect(card.lines.join("\n")).toContain(`本轮执行 ${status === "executed" ? 1 : 0} 个实验子程序`)
+  }
+})
+
 test("a completed client tool can display a running backend task and unknown billing", () => {
   const card = paasCard({ status: "running", run_id: "run_long", agent_name: "Data67" })
   expect(card.title).toBe("Data67 · 运行中")
@@ -55,3 +115,72 @@ test("explicitly complete usage is distinguished from missing completeness", ()
 test("a stale confirmation cannot hide a terminal backend state", () => {
   expect(paasCard({ status: "cancelled", pending_confirmation: { status: "pending" } }).title).toContain("已取消")
 })
+
+test("resource gaps and missing independent evaluation stay visible after completion", () => {
+  const card = paasCard({ status: "completed", research: { science: {
+    resources: { methods: [{ method: "randomized_trial", status: "no_registered_operation", executors: [] }] },
+    acceptance: { dimensions: { independent_scientific_evaluation: { status: "not_run" }, remote_or_instrument_acceptance: { status: "not_verified" } } },
+  } } })
+  expect(card.lines.join("\n")).toContain("缺少执行适配器")
+  expect(card.lines.join("\n")).toContain("独立科学评价:未开展")
+  expect(card.lines.join("\n")).toContain("不折算科学总分")
+})
+
+test("dataset inventory is not presented as completed scientific execution", () => {
+  const card = paasCard({ status: "completed", research: { science: {
+    resources: { datasets: [{ id: "water", profile: { kind: "table", row_count: 7, malformed_rows: 0 } },
+      { id: "protocol", profile: { kind: "finite_graph", state_count: 4, transition_count: 4 } }] },
+    research_ir: { status: "plans_only", planned_actions: 2, executed_kernels: 0 },
+  } } })
+  const lines = card.lines.join("\n")
+  expect(lines).toContain("输入概况:water · 7 行")
+  expect(lines).toContain("4 个状态 · 4 条转移;不是实验结果")
+  expect(lines).toContain("本轮执行 0 个实验子程序")
+})
+
+test("numeric checks expose extracted scope without declaring complete verification", () => {
+  for (const count of [0, 3]) {
+    const card = paasCard({ status: "completed", research: { science: {
+      synthesis: { status: "reviewed", answered: 1, total: 1, numeric_check: { status: "checked", quantity_claims: count } },
+    } } })
+    expect(card.lines.join("\n")).toContain(`提取 ${count} 条;不代表已覆盖全部表述`)
+  }
+})
+
+test("numeric coverage reports unlinked occurrences separately from claim correctness", () => {
+  const card = paasCard({ status: "completed", research: { science: {
+    synthesis: { status: "reviewed", answered: 1, total: 1, numeric_check: {
+      status: "checked", quantity_claims: 4, artifact: "study-synthesis-1-numbers-checks.json",
+      coverage: { candidate_mentions: 7, linked_mentions: 4, unlinked_mentions: 3 },
+    } },
+  } } })
+  expect(card.lines.join("\n")).toContain("4/7 已关联;3 未关联(可能包含计划值或标识)")
+  expect(card.lines.join("\n")).toContain("study-synthesis-1-numbers-checks.json,不是语义核验覆盖率")
+})
+
+test("supplemental numeric extraction distinguishes recovered, excluded and unfinished checks", () => {
+  const completed = paasCard({ status: "completed", research: { science: { synthesis: {
+    numeric_check: { status: "checked", quantity_claims: 1, followup: { status: "completed",
+      recovered_mentions: 1, not_result_mentions: 2, unresolved_mentions: 3, artifact: "numbers-unlinked.json" } },
+  } } } })
+  expect(completed.lines.join("\n")).toContain("恢复 1 处 · 模型判断非结果 2 处 · 暂无法对应 3 处")
+  const pending = paasCard({ status: "completed", research: { science: { synthesis: {
+    numeric_check: { status: "checked", quantity_claims: 0, followup: { status: "pending" } },
+  } } } })
+  expect(pending.lines.join("\n")).toContain("遗漏补查尚未完成;保留首次核对与未关联项,不表示全文通过")
+})
+
+
+test("result-only followups preserve plan issues and do not advertise a new execution IR", () => {
+  for (const operation of ["review_results", "clarify"]) {
+    const card = paasCard({status:"completed", research:{science:{
+      operation: {operation}, research_ir: {status:"not_executed",executed_kernels:0,planned_actions:0},
+      acceptance:{dimensions:{plan_review:{status:"not_reassessed"}}},
+    }}})
+    const text = card.lines.join("\n")
+    expect(text).toContain(operation==="review_results" ? "复核已有结果" : "等待说明研究意图")
+    expect(text).toContain("原有待修订项继续保留")
+    expect(text).toContain("本轮执行 0 个实验子程序")
+    expect(text).not.toContain("execution-ir.json 查看范围")
+  }
+})