Просмотр исходного кода

feat(tui): 展示重构业务验收并连接操作手册

让规划材料、终端审阅与可视化看板共同展示业务条件和实际测试绑定,减少工程验证与业务验收之间的理解偏差。

- 补齐业务验收提案契约与规划提示,支持源码测试方法标识
- 展示前后契约结果、实际绑定用例数、排除项与缺失依据
- 保留本体失败原因,区分尚未取得结果和明确未声明业务验收
- 增加同伴操作手册入口,说明配套 PaaS 与客户端分支
- CLI 回归 20 项、工具及看板回归 18 项通过,两个包类型检查通过
邵勃源 2 часов назад
Родитель
Сommit
f0b566227c

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

@@ -2,6 +2,12 @@
 
 
 OpenCode 是用户入口;PaaS 管理智能体、模型、资源、执行记录和服务端工作区。本套桥接适用于本仓库的 OpenCode fork,支持返回带 metadata/attachments 的自定义工具。尚未对其他发行版作兼容性承诺。
 OpenCode 是用户入口;PaaS 管理智能体、模型、资源、执行记录和服务端工作区。本套桥接适用于本仓库的 OpenCode fork,支持返回带 metadata/attachments 的自定义工具。尚未对其他发行版作兼容性承诺。
 
 
+## 仓库重构操作手册
+
+从环境准备、OCPP 测试仓库到候选选择、人工审查及交付,见配套 PaaS 分支的
+[重构平台操作手册](https://gogs.seec.seecoder.cn/LiuQin/lambdagentpaas/src/paas-branch-merge/docs/refactor-operation-manual.zh.md)。
+代码位于 `paas-branch-merge` 与本仓库 `bridge-zjc`,请配套更新。看板为只读展示,主操作入口是 `/refactor`。
+
 ## 安装和连接
 ## 安装和连接
 
 
 在本仓库运行,目标可以是任意用户项目,不必从桥接源码目录启动:
 在本仓库运行,目标可以是任意用户项目,不必从桥接源码目录启动:

+ 2 - 0
.opencode/agent/paas.md

@@ -79,3 +79,5 @@ CLI 请求异常:如果面板提示上次请求结果尚未核对,让用户
 重构回复面向用户说明当前结果和一个下一步,不每回合重复整套边界说明。分析中根据真实 coverage/progress 报告已扫描数量与当前建模阶段;未完成时不把局部候选数当最终数量。规划前可以提出保留现状或暂缓的建议,但不能自行改变用户已选项。最终按影响路径、前后变化、测试结果、剩余问题说明,引用实际证据,区分必须修正与可选改善。
 重构回复面向用户说明当前结果和一个下一步,不每回合重复整套边界说明。分析中根据真实 coverage/progress 报告已扫描数量与当前建模阶段;未完成时不把局部候选数当最终数量。规划前可以提出保留现状或暂缓的建议,但不能自行改变用户已选项。最终按影响路径、前后变化、测试结果、剩余问题说明,引用实际证据,区分必须修正与可选改善。
 
 
 本体驱动规划:读取所选候选的 ontology_context,区分 impact_paths(上游影响)与 dependency_paths(下游依赖契约);结合 unresolved_references 判断哪些关系仍需查证。按 reading_guide 读取所选方法、关联测试、依赖源码和相关模块 POM,形成“改动点 → 关系证据 → 保持条件 → 验证依据”的简洁说明。摘要超过 30 条时仍可通过 context.files 和 source 分页读取完整关联源码。Java AST 的同包/泛型/构造类型关系是静态候选,不等于编译器调用或测试覆盖。不同仓库采用其自身构建范围和有出处的规则;没有业务规则时只做结构重构,不编造行业约束。
 本体驱动规划:读取所选候选的 ontology_context,区分 impact_paths(上游影响)与 dependency_paths(下游依赖契约);结合 unresolved_references 判断哪些关系仍需查证。按 reading_guide 读取所选方法、关联测试、依赖源码和相关模块 POM,形成“改动点 → 关系证据 → 保持条件 → 验证依据”的简洁说明。摘要超过 30 条时仍可通过 context.files 和 source 分页读取完整关联源码。Java AST 的同包/泛型/构造类型关系是静态候选,不等于编译器调用或测试覆盖。不同仓库采用其自身构建范围和有出处的规则;没有业务规则时只做结构重构,不编造行业约束。
+
+业务验收:context.proposal_contract.business_acceptance 给出格式及检查类别。有真实需求与已有测试依据时,使用 plan.business_acceptance,将 invariant 逐字绑定 plan.invariants,并填写 test_path、实际 test_case 和断言原文 quote;Maven 首次规划可使用源码中的全限定类名#方法名,不需要猜报告路径;也可引用已有报告的精确用例 ID。平台在执行后绑定实际用例,参数化方法要求所有匹配实例都通过。可选 evidence_id 用于消除断言歧义。exclusions 必须逐项说明项目范围依据,不能把缺测试当排除理由凑通过。缺证据时明确业务尚未验证;不要新增伪造测试、编造车企约束。向用户分别展示构建、回归、业务契约及排除项,提交声明不等于验收通过。

Разница между файлами не показана из-за своего большого размера
+ 8 - 1
.opencode/lib/software-dashboard-ui.ts


+ 49 - 0
.opencode/lib/software-review-contract.ts

@@ -1,6 +1,50 @@
 import { tool } from "@opencode-ai/plugin"
 import { tool } from "@opencode-ai/plugin"
 
 
 const textList = tool.schema.array(tool.schema.string().min(1).max(2000)).min(1).max(50)
 const textList = tool.schema.array(tool.schema.string().min(1).max(2000)).min(1).max(50)
+const businessCategory = tool.schema.enum([
+  "input_validation",
+  "write_scope",
+  "resource_ownership",
+  "workflow_integrity",
+  "query_context",
+  "units_ranges",
+  "variant_revision",
+  "protocol_compatibility",
+  "retry_idempotency",
+  "audit_traceability",
+])
+const evidenceText = tool.schema.string().min(1).max(8000)
+const businessAcceptance = tool.schema
+  .object({
+    profile: tool.schema.enum(["general", "automotive"]),
+    contracts: tool.schema
+      .array(
+        tool.schema
+          .object({
+            id: tool.schema.string().regex(/^[A-Za-z][\w-]{0,63}$/),
+            category: businessCategory,
+            invariant: evidenceText.describe("必须逐字引用 plan.invariants 中的保持条件"),
+            test_path: evidenceText.describe("仓库内已有且本轮不可修改的测试源码路径"),
+            test_case: evidenceText.describe("Maven 使用源码全限定类名#方法名,或已有报告中的精确用例ID;java-main 使用 main_class。平台执行后绑定真实用例。"),
+            quote: evidenceText.describe("真实测试中的断言原文,不是注释或说明"),
+            evidence_id: evidenceText.optional(),
+          })
+          .strict(),
+      )
+      .min(1)
+      .max(100),
+    exclusions: tool.schema
+      .array(
+        tool.schema
+          .object({
+            category: businessCategory,
+            reason: evidenceText,
+          })
+          .strict(),
+      )
+      .max(10),
+  })
+  .strict()
 export const reviewPlan = tool.schema
 export const reviewPlan = tool.schema
   .object({
   .object({
     project: tool.schema.string().min(1).max(160),
     project: tool.schema.string().min(1).max(160),
@@ -33,6 +77,11 @@ export const reviewPlan = tool.schema
       "复制 context.required_focus 中的真实本体节点 ID;服务端按已选候选绑定。修改说明放 goal,不要把自然语言当节点名。",
       "复制 context.required_focus 中的真实本体节点 ID;服务端按已选候选绑定。修改说明放 goal,不要把自然语言当节点名。",
     ),
     ),
     invariants: textList.describe("待验证的行为保持条件,不表示已证明"),
     invariants: textList.describe("待验证的行为保持条件,不表示已证明"),
+    business_acceptance: businessAcceptance
+      .optional()
+      .describe(
+        "有真实测试依据时声明业务验收:保持条件绑定测试与断言;缺失类别须说明范围排除理由。未声明不能称业务通过。服务端执行并核验绑定。",
+      ),
     verification: tool.schema
     verification: tool.schema
       .union([
       .union([
         tool.schema
         tool.schema

+ 37 - 0
packages/opencode/test/tool/software-dashboard.test.ts

@@ -41,3 +41,40 @@ test("dashboard script parses without build dependencies or external content", (
   expect(html).toContain('重构观察台')
   expect(html).toContain('重构观察台')
   expect(html).not.toContain('\\u91CD')
   expect(html).not.toContain('\\u91CD')
 })
 })
+
+test("dashboard explains actual business contracts, exclusions and missing evidence", () => {
+  const script = softwareDashboardHTML("b".repeat(64)).match(/<script>([\s\S]*)<\/script>/)![1]
+  // Execute the shipped render functions, omitting browser event registration.
+  const render = new Function("value", "planned", script.split("document.addEventListener('click'")[0] +
+    ";return businessAcceptance(value, planned)")
+  const spec = { profile: "general", passed: false, contracts: [{ category: "workflow_integrity",
+    invariant: "Keep <value>", test_path: "Check.java", test_case: "Check#total", status: "unverified",
+    quote: "assert total(2) == 2;", reason: "Test did not execute" }],
+    exclusions: [{ category: "query_context", reason: "No query subsystem" }], missing_categories: ["write_scope"] }
+  const html = render(spec, false)
+  expect(html).toContain("Check#total")
+  expect(html).toContain("Keep &lt;value&gt;")
+  expect(html).toContain("Test did not execute")
+  expect(html).toContain("排除 · 查询上下文")
+  expect(html).toContain("缺项 · 写入范围")
+  expect(html).not.toContain("声明范围通过")
+  expect(render(spec, true)).toContain(spec.contracts[0].quote)
+  expect(render(undefined, false)).toContain("尚未取得业务验收结果")
+  expect(render(undefined, true)).toContain("未声明业务验收")
+  expect(render({ status: "not_declared" }, false)).toContain("未声明业务验收")
+  expect(script).toContain("businessAcceptance(p.plan.business_acceptance,true)")
+  expect(render({ ...spec, contracts: [{ ...spec.contracts[0], executed_test_cases: ["one", "two"] }] }, false))
+    .toContain("已绑定 2 个实际执行用例")
+})
+
+test("dashboard keeps a failed ontology verdict alongside passing native tests", () => {
+  const script = softwareDashboardHTML("b".repeat(64)).match(/<script>([\s\S]*)<\/script>/)![1]
+  const render = new Function("review", "evidence", script.split("document.addEventListener('click'")[0] +
+    ";state=review;index=evidence;return tests()")
+  const html = render({ execution: { result: {} } }, { files: [], checks: {
+    "round-1/checks.json": { passed: false, build_status: "passed", test_status: "passed",
+      ontology_error: "Rule <target> no longer resolves" },
+  } })
+  expect(html).toContain("本体重建失败:Rule &lt;target&gt; no longer resolves")
+  expect(html).toContain("通过 / 通过")
+})

+ 29 - 0
packages/opencode/test/tool/software-review.test.ts

@@ -105,6 +105,35 @@ const plan = {
 }
 }
 const edits = [{ path: "Price.java", old: "if ((x = n) > 0)", new: "x = n; if (x > 0)" }]
 const edits = [{ path: "Price.java", old: "if ((x = n) > 0)", new: "x = n; if (x > 0)" }]
 
 
+test("business acceptance reaches the proposal contract without losing exact evidence", () => {
+  for (const profile of ["general", "automotive"] as const) {
+    const business_acceptance = {
+      profile,
+      contracts: [{ id: "return-value", category: "workflow_integrity" as const, invariant: plan.invariants[0],
+        test_path: "src/test/java/PriceTest.java", test_case: "PriceTest#signedValues",
+        quote: "assertEquals(4, Price.total(2));" }],
+      exclusions: [{ category: "query_context" as const, reason: "No query subsystem in this synthetic fixture" }],
+    }
+    const input = { ...plan, business_acceptance }
+    expect(parseReviewProposal({ revision: 2, plan: input, edits }).plan.business_acceptance).toEqual(business_acceptance)
+    expect(parseReviewProposal({ revision: 2, proposal_json: JSON.stringify({ plan: input, edits }) }).plan).toEqual(input)
+    expect(() => parseReviewProposal({ revision: 2, plan: { ...input, business_acceptance: {
+      ...business_acceptance, contracts: [{ ...business_acceptance.contracts[0], quote: undefined }],
+    } }, edits })).toThrow("quote")
+  }
+})
+
+test("review pagination exposes every persisted candidate beyond the old cap", () => {
+  const candidates = Array.from({ length: 65 }, (_, i) => ({ id: String(i), path: "Many.java" }))
+  const state = { candidates, decisions: {} }
+  const first = reviewSummary(state, 0, 50)
+  const last = reviewSummary(state, 50, 50)
+  expect(first.candidates_total).toBe(65)
+  expect(first.next_offset).toBe(50)
+  expect(last.next_offset).toBeNull()
+  expect([...first.candidates, ...last.candidates].map(c => c.id)).toEqual(candidates.map(c => c.id))
+})
+
 test("proposal exposes exact structured contract and rejects transcript-shaped guessing", () => {
 test("proposal exposes exact structured contract and rejects transcript-shaped guessing", () => {
   expect(parseReviewProposal({ revision: 2, plan, edits })).toEqual({ plan, edits })
   expect(parseReviewProposal({ revision: 2, plan, edits })).toEqual({ plan, edits })
   expect(parseReviewProposal({ revision: 2, proposal_json: JSON.stringify({ plan, edits }) })).toEqual({ plan, edits })
   expect(parseReviewProposal({ revision: 2, proposal_json: JSON.stringify({ plan, edits }) })).toEqual({ plan, edits })

+ 1 - 1
packages/tui/src/component/dialog-software-review.tsx

@@ -753,7 +753,7 @@ export function DialogSoftwareReview(props: {
       .join("\n\n")
       .join("\n\n")
   }
   }
   const planningPrompt = () =>
   const planningPrompt = () =>
-    `请读取审阅任务 ${props.reviewID} 的最新 context,基于我已保存的候选选择和修改范围生成重构提案。若有 previous_attempt,先读取失败证据与退回意见,说明本次怎样修正。按 reading_guide 分页读取所选方法、上游影响、下游依赖契约、测试和构建文件;必要时用 ontology 的 relation_view 分别查看 impact、dependencies、unresolved。用“改动点 → 关系证据 → 保持条件 → 验证依据”解释方案,说明收益和剩余缺口。只生成计划,不执行。`
+    `请读取审阅任务 ${props.reviewID} 的最新 context,基于我已保存的候选选择和修改范围生成重构提案。若有 previous_attempt,先读取失败证据与退回意见,说明本次怎样修正。按 reading_guide 分页读取所选方法、上游影响、下游依赖契约、测试和构建文件;必要时用 ontology 的 relation_view 分别查看 impact、dependencies、unresolved。用“改动点 → 关系证据 → 保持条件 → 验证依据”解释方案,说明收益和剩余缺口。有真实需求和已有断言依据时,按 context 的 business_acceptance 契约绑定业务验收;没有依据则明确未验证,不编造规则或排除理由。只生成计划,不执行。`
   const body = () => {
   const body = () => {
     if (mode() === "quality-summary")
     if (mode() === "quality-summary")
       return [
       return [

+ 51 - 0
packages/tui/src/util/software-review-business.ts

@@ -0,0 +1,51 @@
+const object = (value: unknown): Record<string, unknown> =>
+  value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {}
+const rows = (value: unknown) => (Array.isArray(value) ? value.map(object) : [])
+const text = (value: unknown) => (typeof value === "string" ? value.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "") : "未提供")
+const categories: Record<string, string> = {
+  input_validation: "输入校验",
+  write_scope: "写入范围",
+  resource_ownership: "资源归属",
+  workflow_integrity: "业务流程完整性",
+  query_context: "查询上下文",
+  units_ranges: "单位与取值范围",
+  variant_revision: "车型与版本",
+  protocol_compatibility: "协议兼容",
+  retry_idempotency: "重试与幂等",
+  audit_traceability: "审计可追溯",
+}
+const category = (value: unknown) => categories[text(value)] || text(value)
+const statuses: Record<string, string> = {
+  verified: "已绑定通过的测试",
+  unverified: "未验证",
+  incomplete: "未通过",
+  verified_with_exclusions: "声明范围通过(含排除项)",
+  verified_declared_scope: "声明范围通过",
+  not_declared: "未声明",
+}
+
+export function businessText(value: unknown, mode: "plan" | "result") {
+  const spec = object(value)
+  if (!Object.keys(spec).length && mode === "result") return "业务验收:尚未取得结果。"
+  if (!Object.keys(spec).length || spec.status === "not_declared")
+    return "业务验收:未声明;构建和回归通过不代表业务范围已验证。"
+  return [
+    mode === "plan" ? "业务验收计划 · 待执行" : `业务验收:${statuses[text(spec.status)] || "未验证"}`,
+    `检查维度:${spec.profile === "automotive" ? "汽车行业" : spec.profile === "general" ? "通用" : "未提供"}`,
+    ...rows(spec.contracts).map((c) =>
+      [
+        `${category(c.category)} · ${text(c.invariant)}`,
+        `  测试:${text(c.test_path)} → ${text(c.test_case)}`,
+        mode === "plan" ? `  断言:${text(c.quote)}` : `  结果:${statuses[text(c.status)] || "未验证"}`,
+        Array.isArray(c.executed_test_cases) ? `  已绑定 ${c.executed_test_cases.length} 个实际执行用例(完整标识见业务验收证据)` : "",
+        c.reason ? `  原因:${text(c.reason)}` : "",
+        object(c.evidence).evidence_id ? `  证据:${text(object(c.evidence).evidence_id)}` : "",
+      ]
+        .filter(Boolean)
+        .join("\n"),
+    ),
+    ...rows(spec.exclusions).map((e) => `排除 · ${category(e.category)}:${text(e.reason)}`),
+    ...(Array.isArray(spec.missing_categories) ? spec.missing_categories.map((c) => `缺项 · ${category(c)}`) : []),
+    "声明与测试的绑定,不代表全部业务正确性;排除项不计为通过。",
+  ].join("\n")
+}

+ 5 - 1
packages/tui/src/util/software-review-report.ts

@@ -1,5 +1,6 @@
 import type { Evidence, Review } from "./software-review"
 import type { Evidence, Review } from "./software-review"
 import { reviewNavigation } from "./software-review-navigation"
 import { reviewNavigation } from "./software-review-navigation"
+import { businessText } from "./software-review-business"
 
 
 export const reportSections = ["影响路径", "前后变化", "测试结果", "剩余问题"] as const
 export const reportSections = ["影响路径", "前后变化", "测试结果", "剩余问题"] as const
 export type ReportSection = 0 | 1 | 2 | 3
 export type ReportSection = 0 | 1 | 2 | 3
@@ -210,6 +211,7 @@ export function reviewReport(s: Review, section: ReportSection, data: ReportData
             check.failed_cases_truncated ? "失败用例未完全展开,请查看完整证据。" : "",
             check.failed_cases_truncated ? "失败用例未完全展开,请查看完整证据。" : "",
             check.build_reason ? `构建原因:${text(check.build_reason)}` : "",
             check.build_reason ? `构建原因:${text(check.build_reason)}` : "",
             check.test_reason ? `测试原因:${text(check.test_reason)}` : "",
             check.test_reason ? `测试原因:${text(check.test_reason)}` : "",
+            check.ontology_error ? `本体重建失败:${text(check.ontology_error)}` : "",
             `证据:${file}`,
             `证据:${file}`,
           ]
           ]
             .filter(Boolean)
             .filter(Boolean)
@@ -220,7 +222,8 @@ export function reviewReport(s: Review, section: ReportSection, data: ReportData
       warning,
       warning,
       describe("重构前基线", baseline, "baseline/checks.json"),
       describe("重构前基线", baseline, "baseline/checks.json"),
       describe("重构后", current, `round-${result.generation || "?"}/checks.json`),
       describe("重构后", current, `round-${result.generation || "?"}/checks.json`),
-      `业务验收:${status(business.status)}${business.passed === true ? "(仅声明范围)" : ";未验证部分不能计为通过"}`,
+      "重构前 · " + businessText(object(result.business_acceptance).baseline, "result"),
+      "重构后 · " + businessText(business, "result"),
       "测试通过只支持已执行的验证范围,不代表全分支覆盖、生产验收或所有业务等价。",
       "测试通过只支持已执行的验证范围,不代表全分支覆盖、生产验收或所有业务等价。",
       `日志与断言:在「证据文件与下载」检查 build.log、test.log、test-evidence.json。`,
       `日志与断言:在「证据文件与下载」检查 build.log、test.log、test-evidence.json。`,
     ]
     ]
@@ -238,6 +241,7 @@ export function reviewReport(s: Review, section: ReportSection, data: ReportData
     header,
     header,
     warning,
     warning,
     s.error ? "执行问题\n" + s.error : "",
     s.error ? "执行问题\n" + s.error : "",
+    current.ontology_error ? "本体重建问题\n" + text(current.ontology_error) : "",
     "环境阻塞\n" +
     "环境阻塞\n" +
       limited(
       limited(
         rows(readiness.blockers).map(
         rows(readiness.blockers).map(

+ 3 - 0
packages/tui/src/util/software-review.ts

@@ -2,6 +2,7 @@ import path from "node:path"
 import { homedir } from "node:os"
 import { homedir } from "node:os"
 import { mkdtemp, mkdir, writeFile, rename, rm, open } from "node:fs/promises"
 import { mkdtemp, mkdir, writeFile, rename, rm, open } from "node:fs/promises"
 import { createHash, randomUUID } from "node:crypto"
 import { createHash, randomUUID } from "node:crypto"
+import { businessText } from "./software-review-business"
 export type Decision = "selected" | "deferred" | "retained"
 export type Decision = "selected" | "deferred" | "retained"
 export type Review = {
 export type Review = {
   id: string
   id: string
@@ -36,6 +37,7 @@ export type Review = {
       new_files?: string[]
       new_files?: string[]
       shared_implementations?: unknown[]
       shared_implementations?: unknown[]
       review_rules?: unknown[]
       review_rules?: unknown[]
+      business_acceptance?: unknown
       verification: unknown
       verification: unknown
     }
     }
     edits: { path: string; old: string; new: string }[]
     edits: { path: string; old: string; new: string }[]
@@ -183,6 +185,7 @@ export function proposalText(s: Review) {
     `本体目标\n${p.plan.focus.join("\n")}`,
     `本体目标\n${p.plan.focus.join("\n")}`,
     `行为保持条件(待验证)\n${p.plan.invariants.join("\n")}`,
     `行为保持条件(待验证)\n${p.plan.invariants.join("\n")}`,
     `验证配置\n${JSON.stringify(p.plan.verification, null, 2)}`,
     `验证配置\n${JSON.stringify(p.plan.verification, null, 2)}`,
+    businessText(p.plan.business_acceptance, "plan"),
     `文档绑定的业务/数据规则(声明,不代表验证通过)\n${JSON.stringify(p.plan.review_rules || [], null, 2)}`,
     `文档绑定的业务/数据规则(声明,不代表验证通过)\n${JSON.stringify(p.plan.review_rules || [], null, 2)}`,
     `环境预检(不代表测试通过)\n${JSON.stringify(p.readiness || { status: "未检查" }, null, 2)}`,
     `环境预检(不代表测试通过)\n${JSON.stringify(p.readiness || { status: "未检查" }, null, 2)}`,
     ...p.edits.map((e, i) => `补丁 ${i + 1}/${p.edits.length} · ${e.path}\n--- 修改前\n${e.old}\n+++ 修改后\n${e.new}`),
     ...p.edits.map((e, i) => `补丁 ${i + 1}/${p.edits.length} · ${e.path}\n--- 修改前\n${e.old}\n+++ 修改后\n${e.new}`),

+ 32 - 0
packages/tui/test/software-review-report.test.ts

@@ -1,6 +1,7 @@
 import { expect, test } from "bun:test"
 import { expect, test } from "bun:test"
 import { candidateImpact, nextReviewStep, reviewReport } from "../src/util/software-review-report"
 import { candidateImpact, nextReviewStep, reviewReport } from "../src/util/software-review-report"
 import type { Review, Evidence } from "../src/util/software-review"
 import type { Review, Evidence } from "../src/util/software-review"
+import { businessText } from "../src/util/software-review-business"
 
 
 const fixture = (): Review => ({
 const fixture = (): Review => ({
   id: "a".repeat(32),
   id: "a".repeat(32),
@@ -33,6 +34,37 @@ const comparison = {
   },
   },
 }
 }
 
 
+test("business review shows test bindings and exclusions before and after execution", () => {
+  const spec = { profile: "automotive", status: "incomplete", passed: false,
+    contracts: [{ category: "units_ranges", invariant: "Keep units", test_path: "Check.java", test_case: "Check#units",
+      quote: "assert speed == 36;", status: "unverified", reason: "Assertion did not match" }],
+    exclusions: [{ category: "variant_revision", reason: "No variant field in this fixture" }],
+    missing_categories: ["protocol_compatibility"] }
+  expect(businessText(spec, "plan")).toContain("断言:assert speed == 36;")
+  const state = fixture()
+  state.execution = { result: { business_acceptance: { baseline: spec, current: spec } } }
+  const report = reviewReport(state, 2)
+  for (const value of ["重构前 · 业务验收", "重构后 · 业务验收", "Check.java → Check#units", "Assertion did not match",
+    "排除 · 车型与版本", "缺项 · 协议兼容"]) expect(report).toContain(value)
+  expect(report).not.toContain("声明范围通过")
+  expect(businessText(undefined, "result")).toContain("尚未取得结果")
+  expect(businessText(undefined, "plan")).toContain("未声明")
+  expect(businessText({ status: "not_declared" }, "result")).toContain("未声明")
+  expect(businessText({ ...spec, contracts: [{ ...spec.contracts[0], executed_test_cases: ["actual::one", "actual::two"] }] }, "result"))
+    .toContain("已绑定 2 个实际执行用例")
+})
+
+test("ontology failure stays visible even when build and native tests pass", () => {
+  const state = fixture()
+  state.phase = "verification_failed"
+  const evidence: Evidence = { id: state.id, revision: state.revision, token: "t", files: [], checks: {
+    "round-1/checks.json": { passed: false, build_status: "passed", test_status: "passed",
+      ontology_error: "Declared rule target no longer resolves" },
+  } }
+  expect(reviewReport(state, 2, { evidence })).toContain("本体重建失败:Declared rule target no longer resolves")
+  expect(reviewReport(state, 3, { evidence })).toContain("本体重建问题")
+})
+
 test("reports residual extracted methods without upgrading a disappearing finding to success", () => {
 test("reports residual extracted methods without upgrading a disappearing finding to success", () => {
   const value = reviewReport(fixture(), 1, { comparison })
   const value = reviewReport(fixture(), 1, { comparison })
   expect(value).toContain("106 行")
   expect(value).toContain("106 行")

Некоторые файлы не были показаны из-за большого количества измененных файлов