software-review-contract.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import { tool } from "@opencode-ai/plugin"
  2. const textList = tool.schema.array(tool.schema.string().min(1).max(2000)).min(1).max(50)
  3. const businessCategory = tool.schema.enum([
  4. "input_validation",
  5. "write_scope",
  6. "resource_ownership",
  7. "workflow_integrity",
  8. "query_context",
  9. "units_ranges",
  10. "variant_revision",
  11. "protocol_compatibility",
  12. "retry_idempotency",
  13. "audit_traceability",
  14. ])
  15. const evidenceText = tool.schema.string().min(1).max(8000)
  16. const businessAcceptance = tool.schema
  17. .object({
  18. profile: tool.schema.enum(["general", "automotive"]),
  19. contracts: tool.schema
  20. .array(
  21. tool.schema
  22. .object({
  23. id: tool.schema.string().regex(/^[A-Za-z][\w-]{0,63}$/),
  24. category: businessCategory,
  25. invariant: evidenceText.describe("必须逐字引用 plan.invariants 中的保持条件"),
  26. test_path: evidenceText.describe("仓库内已有且本轮不可修改的测试源码路径"),
  27. test_case: evidenceText.describe("Maven 使用源码全限定类名#方法名,或已有报告中的精确用例ID;java-main 使用 main_class。平台执行后绑定真实用例。"),
  28. quote: evidenceText.describe("真实测试中的断言原文,不是注释或说明"),
  29. evidence_id: evidenceText.optional(),
  30. })
  31. .strict(),
  32. )
  33. .min(1)
  34. .max(100),
  35. exclusions: tool.schema
  36. .array(
  37. tool.schema
  38. .object({
  39. category: businessCategory,
  40. reason: evidenceText,
  41. })
  42. .strict(),
  43. )
  44. .max(10),
  45. })
  46. .strict()
  47. export const reviewPlan = tool.schema
  48. .object({
  49. project: tool.schema.string().min(1).max(160),
  50. goal: tool.schema.string().min(1).max(4000),
  51. candidate_ids: tool.schema
  52. .array(tool.schema.string().regex(/^[a-f0-9]{24}$/))
  53. .min(1)
  54. .max(50)
  55. .describe("必须包含用户当前选中的全部候选;不可擅自删减"),
  56. new_files: tool.schema
  57. .array(tool.schema.string().min(1))
  58. .max(20)
  59. .optional()
  60. .describe("仅用户选择 structure 范围时,可声明 Maven src/main/java 新生产类;old 必须为空,new 为完整源码"),
  61. shared_implementations: tool.schema
  62. .array(
  63. tool.schema
  64. .object({
  65. target_path: tool.schema.string(),
  66. method: tool.schema.string(),
  67. caller_paths: tool.schema.array(tool.schema.string()).min(2),
  68. })
  69. .strict(),
  70. )
  71. .max(10)
  72. .optional()
  73. .describe("抽取共享方法时声明目标方法与至少两个调用文件,编译器检查真实调用"),
  74. allowed_files: textList.describe("全部已选候选对应的现有 Java 源文件;structure 模式可另加明确声明的 new_files"),
  75. focus: textList.describe(
  76. "复制 context.required_focus 中的真实本体节点 ID;服务端按已选候选绑定。修改说明放 goal,不要把自然语言当节点名。",
  77. ),
  78. invariants: textList.describe("待验证的行为保持条件,不表示已证明"),
  79. business_acceptance: businessAcceptance
  80. .optional()
  81. .describe(
  82. "有真实测试依据时声明业务验收:保持条件绑定测试与断言;缺失类别须说明范围排除理由。未声明不能称业务通过。服务端执行并核验绑定。",
  83. ),
  84. verification: tool.schema
  85. .union([
  86. tool.schema
  87. .object({
  88. kind: tool.schema.literal("maven-offline"),
  89. pom_path: tool.schema.string().optional().describe("真实仓库相对 pom.xml 路径;省略则使用根 pom.xml"),
  90. })
  91. .strict(),
  92. tool.schema
  93. .object({
  94. kind: tool.schema.literal("java-main"),
  95. sources: tool.schema.array(tool.schema.string()).min(1).max(100),
  96. test_sources: tool.schema.array(tool.schema.string()).min(1).max(100),
  97. main_class: tool.schema.string(),
  98. })
  99. .strict(),
  100. ])
  101. .describe(
  102. "Maven 使用 {kind:'maven-offline',pom_path:'pom.xml'};执行固定的离线 package/test,不能指定 command 或声称只跑某个测试。java-main 只能引用已存在的真实测试文件。",
  103. ),
  104. })
  105. .strict()
  106. export const reviewEdits = tool.schema
  107. .array(
  108. tool.schema
  109. .object({
  110. path: tool.schema.string(),
  111. old: tool.schema.string().max(256000).describe("当前源码中恰好出现一次的原文;字段为 old,不是 old_string"),
  112. new: tool.schema.string().max(256000).describe("替换后的原文;字段为 new,不是 new_string"),
  113. })
  114. .strict(),
  115. )
  116. .min(1)
  117. .max(30)
  118. const proposal = tool.schema.object({ plan: reviewPlan, edits: reviewEdits }).strict()
  119. export function parseReviewProposal(args: {
  120. revision?: number
  121. plan?: unknown
  122. edits?: unknown
  123. proposal_json?: string
  124. }) {
  125. if (!Number.isInteger(args.revision) || args.revision! < 0)
  126. throw new Error("propose 必须单独传顶层 revision,取 context.revision;不能放进 proposal_json。")
  127. if (args.proposal_json && (args.plan !== undefined || args.edits !== undefined))
  128. throw new Error("使用 plan + edits 或兼容的 proposal_json,不能同时传两种格式。")
  129. if (args.proposal_json && args.proposal_json.length > 1024 * 1024)
  130. throw new Error("提案超过 1 MiB,请缩小经用户选择的批次。")
  131. const parsed = proposal.safeParse(
  132. args.proposal_json ? JSON.parse(args.proposal_json) : { plan: args.plan, edits: args.edits },
  133. )
  134. if (!parsed.success)
  135. throw new Error(
  136. `提案未发送:${parsed.error.issues
  137. .slice(0, 8)
  138. .map((i) => `${i.path.join(".")}: ${i.message}`)
  139. .join(
  140. "; ",
  141. )}。请使用结构化 plan/edits;focus 为数组,verification 为 {kind:'maven-offline',pom_path:'pom.xml'} 或 {kind:'java-main',sources:[...],test_sources:[...],main_class:'...'};替换字段是 path/old/new。不要猜字段或添加 command。`,
  142. )
  143. return parsed.data
  144. }
  145. function ontologySummary(value: unknown) {
  146. if (!value || typeof value !== "object") return value
  147. const o = value as Record<string, unknown>
  148. return {
  149. ...o,
  150. architecture_findings: undefined,
  151. unresolved_sample: undefined,
  152. inspect: "完整架构线索、未解析关系和证据可在 /refactor 的本体与架构分析中查看;不视为可执行候选。",
  153. }
  154. }
  155. function candidateSummary(value: Record<string, unknown>) {
  156. const o = value.ontology_context as Record<string, unknown> | undefined
  157. if (!o) return value
  158. const paths = Array.isArray(o.impact_paths) ? o.impact_paths : []
  159. const dependencies = Array.isArray(o.dependency_paths) ? o.dependency_paths : undefined
  160. const unresolved = Array.isArray(o.unresolved_references) ? o.unresolved_references : undefined
  161. return {
  162. ...value,
  163. ontology_context: {
  164. ...o,
  165. impact_paths: paths
  166. .slice(0, 3)
  167. .map((p) => ({ path: p.path, via_total: p.via_total, via_truncated: true, via: p.via.slice(0, 3) })),
  168. impact_paths_truncated: Boolean(o.impact_paths_truncated) || paths.length > 3,
  169. dependency_paths: dependencies?.slice(0, 3),
  170. dependency_paths_truncated: Boolean(o.dependency_paths_truncated) || (dependencies?.length || 0) > 3,
  171. unresolved_references: unresolved?.slice(0, 3),
  172. unresolved_references_truncated: Boolean(o.unresolved_references_truncated) || (unresolved?.length || 0) > 3,
  173. full_evidence:
  174. "用 ontology + candidate_id + relation_view 分页查看 impact/dependencies/unresolved;服务端截断时用 evidence/artifact 查完整本体。此处只是导航摘要。",
  175. },
  176. }
  177. }
  178. export function reviewSummary(state: Record<string, unknown>, offset = 0, limit = 8) {
  179. const candidates = Array.isArray(state.candidates) ? (state.candidates as Record<string, unknown>[]) : []
  180. const decisions = state.decisions as Record<string, unknown> | undefined
  181. const proposal = state.proposal as Record<string, unknown> | undefined
  182. return {
  183. id: state.id,
  184. revision: state.revision,
  185. phase: state.phase,
  186. project: state.project,
  187. coverage: state.coverage,
  188. progress: state.progress,
  189. analysis_scope: state.analysis_scope,
  190. repository_summary: state.repository_summary,
  191. edit_scope: state.edit_scope || "methods",
  192. next_review: state.next_review,
  193. quality_review: state.quality_review,
  194. cancel_requested: state.cancel_requested,
  195. error: state.error,
  196. ontology: ontologySummary(state.ontology),
  197. recovery:
  198. !proposal && ["analysis_failed", "cancelled", "interrupted"].includes(String(state.phase))
  199. ? state.next_review
  200. ? "分析已停止,后续任务已创建;打开 next_review 查看,不能重复创建或返回补丁规划。"
  201. : "分析未完成。请用户在 /refactor 查看原因后选择保留失败记录、重新分析;不能进入补丁规划。"
  202. : state.phase === "evidence_failed"
  203. ? "执行结果已保存,但证据整理失败。请用户在 /refactor 选择重新整理已执行的证据;此操作不重跑测试或补丁,不要重新提案。是否通过以保存的验证结果为准。"
  204. : [
  205. "execution_failed",
  206. "blocked",
  207. "verification_failed",
  208. "changes_requested",
  209. "interrupted",
  210. "cancelled",
  211. ].includes(String(state.phase))
  212. ? "请用户在终端 /refactor 面板选择保留失败记录、返回规划;随后读取 context 生成新提案。不可自动重试或复用旧审批。"
  213. : null,
  214. selected_ids: candidates.filter((c) => decisions?.[String(c.id)] === "selected").map((c) => c.id),
  215. candidates_total: candidates.length,
  216. offset,
  217. next_offset: offset + limit < candidates.length ? offset + limit : null,
  218. candidates: candidates.slice(offset, offset + limit).map(candidateSummary),
  219. proposal: proposal
  220. ? {
  221. version: proposal.version,
  222. plan: proposal.plan,
  223. selected_ids: proposal.selected_ids,
  224. readiness: proposal.readiness,
  225. ontology_binding: proposal.ontology_binding,
  226. focus_notes: proposal.focus_notes,
  227. edit_count: Array.isArray(proposal.edits) ? proposal.edits.length : 0,
  228. inspect: "终端 /refactor 面板显示全部精确补丁,确认前逐项检查",
  229. }
  230. : null,
  231. execution: state.execution,
  232. note: "候选已分页;没有测试提示不代表没有测试覆盖,提示也不能证明分支覆盖。",
  233. }
  234. }
  235. export function reviewContext(state: Record<string, unknown>) {
  236. const files = state.files as Record<string, string>
  237. return {
  238. ...state,
  239. ontology: ontologySummary(state.ontology),
  240. selected: Array.isArray(state.selected) ? state.selected.map(candidateSummary) : state.selected,
  241. files: Array.isArray(state.files)
  242. ? state.files
  243. : Object.entries(files).map(([path, text]) => ({
  244. path,
  245. lines: text.split("\n").length,
  246. characters: text.length,
  247. })),
  248. source_reading:
  249. "这里只返回文件索引,避免大文件截断。提案前使用 source action 按 path/start_line/limit 分页读取方法、测试和 pom.xml;只基于实际已读源码。",
  250. }
  251. }
  252. export function reviewOntology(
  253. state: Record<string, unknown>,
  254. candidateID?: string,
  255. offset = 0,
  256. limit = 5,
  257. view: "impact" | "dependencies" | "unresolved" = "impact",
  258. ) {
  259. if (!candidateID && view !== "impact") throw new Error("查看依赖或未解析关系需要 candidate_id;先读取候选清单")
  260. const ontology = state.ontology as Record<string, unknown> | undefined
  261. const candidates = state.candidates as Record<string, unknown>[] | undefined
  262. const candidate = candidateID ? candidates?.find((c) => c.id === candidateID) : undefined
  263. if (candidateID && !candidate) throw new Error("未知候选编号;先读取当前候选清单")
  264. const detail = candidate?.ontology_context as Record<string, unknown> | undefined
  265. const field = { impact: "impact_paths", dependencies: "dependency_paths", unresolved: "unresolved_references" }[view]
  266. const values = (candidateID ? detail?.[field] : ontology?.architecture_findings) as unknown[] | undefined
  267. const result = {
  268. id: state.id,
  269. revision: state.revision,
  270. statistics: ontology?.statistics,
  271. rules_status: ontology?.rules_status,
  272. candidate_id: candidateID,
  273. view: candidateID ? view : "architecture",
  274. authority: "pending_candidate_preview",
  275. status: values ? "available" : "not_available_in_this_analysis",
  276. items: values?.slice(offset, offset + limit) || [],
  277. stored_total: values?.length || 0,
  278. total: candidateID ? detail?.[field + "_total"] : ontology?.architecture_findings_total,
  279. upstream_truncated: candidateID ? detail?.[field + "_truncated"] : ontology?.architecture_findings_truncated,
  280. next_offset: values && offset + limit < values.length ? offset + limit : null,
  281. note: "上游影响、下游依赖和未解析关系分别阅读。静态关联不是运行时调用;外部库未解析不自动判为缺陷。服务端摘要截断时用 context.files/source 读关联源码,用 evidence/artifact 读完整本体。",
  282. }
  283. if (JSON.stringify(result).length > 16000)
  284. throw new Error("本体证据页超过 16000 字符;减小 limit,或在终端审阅完整内容。")
  285. return result
  286. }