import { tool } from "@opencode-ai/plugin" 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 .object({ project: tool.schema.string().min(1).max(160), goal: tool.schema.string().min(1).max(4000), candidate_ids: tool.schema .array(tool.schema.string().regex(/^[a-f0-9]{24}$/)) .min(1) .max(50) .describe("必须包含用户当前选中的全部候选;不可擅自删减"), new_files: tool.schema .array(tool.schema.string().min(1)) .max(20) .optional() .describe("仅用户选择 structure 范围时,可声明 Maven src/main/java 新生产类;old 必须为空,new 为完整源码"), shared_implementations: tool.schema .array( tool.schema .object({ target_path: tool.schema.string(), method: tool.schema.string(), caller_paths: tool.schema.array(tool.schema.string()).min(2), }) .strict(), ) .max(10) .optional() .describe("抽取共享方法时声明目标方法与至少两个调用文件,编译器检查真实调用"), allowed_files: textList.describe("全部已选候选对应的现有 Java 源文件;structure 模式可另加明确声明的 new_files"), focus: textList.describe( "复制 context.required_focus 中的真实本体节点 ID;服务端按已选候选绑定。修改说明放 goal,不要把自然语言当节点名。", ), invariants: textList.describe("待验证的行为保持条件,不表示已证明"), business_acceptance: businessAcceptance .optional() .describe( "有真实测试依据时声明业务验收:保持条件绑定测试与断言;缺失类别须说明范围排除理由。未声明不能称业务通过。服务端执行并核验绑定。", ), verification: tool.schema .union([ tool.schema .object({ kind: tool.schema.literal("maven-offline"), pom_path: tool.schema.string().optional().describe("真实仓库相对 pom.xml 路径;省略则使用根 pom.xml"), }) .strict(), tool.schema .object({ kind: tool.schema.literal("java-main"), sources: tool.schema.array(tool.schema.string()).min(1).max(100), test_sources: tool.schema.array(tool.schema.string()).min(1).max(100), main_class: tool.schema.string(), }) .strict(), ]) .describe( "Maven 使用 {kind:'maven-offline',pom_path:'pom.xml'};执行固定的离线 package/test,不能指定 command 或声称只跑某个测试。java-main 只能引用已存在的真实测试文件。", ), }) .strict() export const reviewEdits = tool.schema .array( tool.schema .object({ path: tool.schema.string(), old: tool.schema.string().max(256000).describe("当前源码中恰好出现一次的原文;字段为 old,不是 old_string"), new: tool.schema.string().max(256000).describe("替换后的原文;字段为 new,不是 new_string"), }) .strict(), ) .min(1) .max(30) const proposal = tool.schema.object({ plan: reviewPlan, edits: reviewEdits }).strict() export function parseReviewProposal(args: { revision?: number plan?: unknown edits?: unknown proposal_json?: string }) { if (!Number.isInteger(args.revision) || args.revision! < 0) throw new Error("propose 必须单独传顶层 revision,取 context.revision;不能放进 proposal_json。") if (args.proposal_json && (args.plan !== undefined || args.edits !== undefined)) throw new Error("使用 plan + edits 或兼容的 proposal_json,不能同时传两种格式。") if (args.proposal_json && args.proposal_json.length > 1024 * 1024) throw new Error("提案超过 1 MiB,请缩小经用户选择的批次。") const parsed = proposal.safeParse( args.proposal_json ? JSON.parse(args.proposal_json) : { plan: args.plan, edits: args.edits }, ) if (!parsed.success) throw new Error( `提案未发送:${parsed.error.issues .slice(0, 8) .map((i) => `${i.path.join(".")}: ${i.message}`) .join( "; ", )}。请使用结构化 plan/edits;focus 为数组,verification 为 {kind:'maven-offline',pom_path:'pom.xml'} 或 {kind:'java-main',sources:[...],test_sources:[...],main_class:'...'};替换字段是 path/old/new。不要猜字段或添加 command。`, ) return parsed.data } function ontologySummary(value: unknown) { if (!value || typeof value !== "object") return value const o = value as Record return { ...o, architecture_findings: undefined, unresolved_sample: undefined, inspect: "完整架构线索、未解析关系和证据可在 /refactor 的本体与架构分析中查看;不视为可执行候选。", } } function candidateSummary(value: Record) { const o = value.ontology_context as Record | undefined if (!o) return value const paths = Array.isArray(o.impact_paths) ? o.impact_paths : [] const dependencies = Array.isArray(o.dependency_paths) ? o.dependency_paths : undefined const unresolved = Array.isArray(o.unresolved_references) ? o.unresolved_references : undefined return { ...value, ontology_context: { ...o, impact_paths: paths .slice(0, 3) .map((p) => ({ path: p.path, via_total: p.via_total, via_truncated: true, via: p.via.slice(0, 3) })), impact_paths_truncated: Boolean(o.impact_paths_truncated) || paths.length > 3, dependency_paths: dependencies?.slice(0, 3), dependency_paths_truncated: Boolean(o.dependency_paths_truncated) || (dependencies?.length || 0) > 3, unresolved_references: unresolved?.slice(0, 3), unresolved_references_truncated: Boolean(o.unresolved_references_truncated) || (unresolved?.length || 0) > 3, full_evidence: "用 ontology + candidate_id + relation_view 分页查看 impact/dependencies/unresolved;服务端截断时用 evidence/artifact 查完整本体。此处只是导航摘要。", }, } } export function reviewSummary(state: Record, offset = 0, limit = 8) { const candidates = Array.isArray(state.candidates) ? (state.candidates as Record[]) : [] const decisions = state.decisions as Record | undefined const proposal = state.proposal as Record | undefined return { id: state.id, revision: state.revision, phase: state.phase, project: state.project, coverage: state.coverage, progress: state.progress, analysis_scope: state.analysis_scope, repository_summary: state.repository_summary, edit_scope: state.edit_scope || "methods", next_review: state.next_review, quality_review: state.quality_review, cancel_requested: state.cancel_requested, error: state.error, ontology: ontologySummary(state.ontology), recovery: !proposal && ["analysis_failed", "cancelled", "interrupted"].includes(String(state.phase)) ? state.next_review ? "分析已停止,后续任务已创建;打开 next_review 查看,不能重复创建或返回补丁规划。" : "分析未完成。请用户在 /refactor 查看原因后选择保留失败记录、重新分析;不能进入补丁规划。" : state.phase === "evidence_failed" ? "执行结果已保存,但证据整理失败。请用户在 /refactor 选择重新整理已执行的证据;此操作不重跑测试或补丁,不要重新提案。是否通过以保存的验证结果为准。" : [ "execution_failed", "blocked", "verification_failed", "changes_requested", "interrupted", "cancelled", ].includes(String(state.phase)) ? "请用户在终端 /refactor 面板选择保留失败记录、返回规划;随后读取 context 生成新提案。不可自动重试或复用旧审批。" : null, selected_ids: candidates.filter((c) => decisions?.[String(c.id)] === "selected").map((c) => c.id), candidates_total: candidates.length, offset, next_offset: offset + limit < candidates.length ? offset + limit : null, candidates: candidates.slice(offset, offset + limit).map(candidateSummary), proposal: proposal ? { version: proposal.version, plan: proposal.plan, selected_ids: proposal.selected_ids, readiness: proposal.readiness, ontology_binding: proposal.ontology_binding, focus_notes: proposal.focus_notes, edit_count: Array.isArray(proposal.edits) ? proposal.edits.length : 0, inspect: "终端 /refactor 面板显示全部精确补丁,确认前逐项检查", } : null, execution: state.execution, note: "候选已分页;没有测试提示不代表没有测试覆盖,提示也不能证明分支覆盖。", } } export function reviewContext(state: Record) { const files = state.files as Record return { ...state, ontology: ontologySummary(state.ontology), selected: Array.isArray(state.selected) ? state.selected.map(candidateSummary) : state.selected, files: Array.isArray(state.files) ? state.files : Object.entries(files).map(([path, text]) => ({ path, lines: text.split("\n").length, characters: text.length, })), source_reading: "这里只返回文件索引,避免大文件截断。提案前使用 source action 按 path/start_line/limit 分页读取方法、测试和 pom.xml;只基于实际已读源码。", } } export function reviewOntology( state: Record, candidateID?: string, offset = 0, limit = 5, view: "impact" | "dependencies" | "unresolved" = "impact", ) { if (!candidateID && view !== "impact") throw new Error("查看依赖或未解析关系需要 candidate_id;先读取候选清单") const ontology = state.ontology as Record | undefined const candidates = state.candidates as Record[] | undefined const candidate = candidateID ? candidates?.find((c) => c.id === candidateID) : undefined if (candidateID && !candidate) throw new Error("未知候选编号;先读取当前候选清单") const detail = candidate?.ontology_context as Record | undefined const field = { impact: "impact_paths", dependencies: "dependency_paths", unresolved: "unresolved_references" }[view] const values = (candidateID ? detail?.[field] : ontology?.architecture_findings) as unknown[] | undefined const result = { id: state.id, revision: state.revision, statistics: ontology?.statistics, rules_status: ontology?.rules_status, candidate_id: candidateID, view: candidateID ? view : "architecture", authority: "pending_candidate_preview", status: values ? "available" : "not_available_in_this_analysis", items: values?.slice(offset, offset + limit) || [], stored_total: values?.length || 0, total: candidateID ? detail?.[field + "_total"] : ontology?.architecture_findings_total, upstream_truncated: candidateID ? detail?.[field + "_truncated"] : ontology?.architecture_findings_truncated, next_offset: values && offset + limit < values.length ? offset + limit : null, note: "上游影响、下游依赖和未解析关系分别阅读。静态关联不是运行时调用;外部库未解析不自动判为缺陷。服务端摘要截断时用 context.files/source 读关联源码,用 evidence/artifact 读完整本体。", } if (JSON.stringify(result).length > 16000) throw new Error("本体证据页超过 16000 字符;减小 limit,或在终端审阅完整内容。") return result }