| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- 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<string, unknown>
- return {
- ...o,
- architecture_findings: undefined,
- unresolved_sample: undefined,
- inspect: "完整架构线索、未解析关系和证据可在 /refactor 的本体与架构分析中查看;不视为可执行候选。",
- }
- }
- function candidateSummary(value: Record<string, unknown>) {
- const o = value.ontology_context as Record<string, unknown> | 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<string, unknown>, offset = 0, limit = 8) {
- const candidates = Array.isArray(state.candidates) ? (state.candidates as Record<string, unknown>[]) : []
- const decisions = state.decisions as Record<string, unknown> | undefined
- const proposal = state.proposal as Record<string, unknown> | 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<string, unknown>) {
- const files = state.files as Record<string, string>
- 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<string, unknown>,
- 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<string, unknown> | undefined
- const candidates = state.candidates as Record<string, unknown>[] | undefined
- const candidate = candidateID ? candidates?.find((c) => c.id === candidateID) : undefined
- if (candidateID && !candidate) throw new Error("未知候选编号;先读取当前候选清单")
- const detail = candidate?.ontology_context as Record<string, unknown> | 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
- }
|