Repository navigation
feat(0.2.0): 自研内核重写 —— 移除 Go/OCR 引擎,改为 Node 内核 + PTC - #38
Merged
Merged
Conversation
added 30 commits
November 4, 2026 03:04
阶段 0 工程地基。 - TypeScript 严格模式配置(类型检查与构建分离) - Vitest 测试框架(unit/integration 分离) - 协议版本真源(task/config/report/kernel 四套版本单点定义) - JSON Schema 运行时校验(落盘 JSON 不可信,不能只靠静态类型) - detached 子进程语义回归测试(0.2.0 内核独立存活的运行前提) 验收:typecheck 通过;9 个测试全绿;构建产物可被 Node 24 直接 import。
阶段 1 领域契约(纯逻辑,零 IO)。 - errors.ts:36 个稳定错误码 + 三层分类(category/retry/effectState),上层只认 code - outcome.ts:Outcome<T> 六状态契约,partial 必须带 coverage+continuation - finding.ts:Finding 唯一结构,行号可信度显式化(exact/diff-resolved/file-resolved/unresolved) - task.ts:任务状态机,11 阶段 + 合法迁移表 + revision 乐观并发 + 心跳过期判定 - review.ts:规模三档(20K/150K)、主 agent 策略、确定性派发规则 验收:typecheck 通过;50 个测试全绿(含非法迁移、边界值、脱敏契约)。
阶段 2 配置真源与无损迁移。 - defaults.ts:v2 分区配置的默认值真源(review/execution/budget/report/sizing/onboarding) - schema.ts:归一化只读不写,返回 raw 供写回合并;越界钳制、非法回退、诊断收集 - migrate.ts:v1→v2 显式迁移链;engine_bin 删除并登记原因;未知字段原样保留 - store.ts:原子写(临时文件+rename)、深合并保留未知键、迁移前备份(保留 3 份)、损坏文件拒绝覆盖 修复 0.1.3 的丢配置缺陷:normalizeConfig 的「未知键剔除」不再存在。 顺带修出真缺陷:first_run 非布尔值曾被误判为「已完成引导」。 验收:typecheck 通过;73 个测试全绿。
阶段 3 确定性工程层(零 LLM 调用,可离线验收)。 - git/command.ts:受限 Git 执行器(参数数组防注入、32MB 输出上限、硬超时、Windows 隐藏窗口) - git/diff-parser.ts:unified diff 状态机解析,精确到每行新旧行号;无法识别的行进 warnings 不静默吞 - git/diff.ts:workspace(含未跟踪文件合成)/ branch(merge-base 语义)/ commit(first-parent,缺 SHA fail-closed)/ scan 枚举 - selection/sensitive.ts:移植 0.1.3 经 issue #11/#24 打磨的敏感模式(保留全部误报修正) - selection/select.ts:七步筛选流水线,每个排除项都有 reason code(拒绝静默排除) 验收:typecheck 通过;145 个测试全绿,含真实临时 Git 仓库集成测试。
阶段 4 PTC 执行沙箱(安全边界)。 - path-fence.ts:三道关(拒绝绝对路径与 ..、realpath 校验符号链接逃逸、只放行普通文件) - types.ts:SDK 参数/结果与预算的唯一真源(同时供 prompt 声明与运行时校验) - read.ts:行数+字节双上限,敏感路径直接拒绝且不读内容,截断返回 partial+continuation - glob.ts:遍历上限、默认跳依赖目录、稳定排序、敏感文件不出现在结果里 - grep.ts:正则长度上限、单行截断、结果上限、敏感文件不参与搜索 - runner-entry.ts:子进程内 vm 受限上下文(无 require/process/fs),返回值字节上限 - runner-client.ts:父进程硬超时 kill、内存上限、stdout 上限、崩溃归一 实测验证:死循环被 1.5s 硬超时终止且父进程存活;程序内 typeof require === 'undefined'。 验收:typecheck 通过;181 个测试全绿。
阶段 5 LLM 客户端(红线所在)。 - protocol.ts:OpenAI chat/completions 最小面类型(不提前实现 responses/anthropic) - client.ts:单请求超时与任务超时解耦;只对 408/429/5xx 与网络错误重试;401/403 不重试直接给配置指引 - usage 归一:缺字段返回 null,绝不用 0 冒充(避免报告把未知用量显示成免费) - 红线落实:端点或凭据为空时在发请求之前就失败,代码中不存在任何默认端点兜底 实测:本地假端点验证 429 重试两次后成功、401 只调用一次、空配置零外呼。 验收:typecheck 通过;194 个测试全绿。
阶段 6 Prompt 与规则(缓存命中率的关键)。 - rules.ts:三条固定视角 + 四种语言规则;按 id 排序保证规则块逐字节稳定 - system.ts:稳定前缀(角色→规则→SDK→输出格式→结束条件),纯函数,不含时间/diff/task id;附哈希函数用于事后验证 - task.ts:易变段(清单→重点→已确认→diff),用边界标记包裹待评审数据以降低 prompt injection 对比 0.1.3:上游把规则放在 diff 之后导致跨组缓存必然失效;这里规则进 system,diff 进 user 尾部。 验收:typecheck 通过;214 个测试全绿(含「同输入同哈希」「system 不含 diff」等稳定性断言)。
阶段 7 单 agent 循环。 - result-parser.ts:模型返回的 JSON 视为不可信输入,逐字段校验;未知枚举/缺 path 丢弃并记录原因;confidence 钳制;超长字段截断 - loop.ts:finish / run_code 两种工具;停止条件五种且各自可区分(finished / max-rounds / budget / no-progress / error) - 关键取舍:模型不调工具时只给一次提醒,第二次直接判 no-progress 结束,避免账单失控 - 预算耗尽标记 budget 而非 finished,结果为空且报告会说明未完成 验收:typecheck 通过;236 个测试全绿(含 429 重试、预算停止、端点故障归一)。
阶段 8 主/子 agent 编排(用户拍板 C:规则打底 + 主 agent 追加)。 - orchestrator/review.ts:取 diff → 筛选 → 定档 → 主 agent → 派子 agent → 合并 - 规模三档落地:小档不派子、中档全局+深挖、大档主 agent 不拿正文 - 派发双通道:确定性规则(行数/token/横向影响文件)+ 主 agent childRequests - 敏感文件在筛选层被排除,只在结果里点名,绝不进 prompt - 状态区分:主 agent 未正常完成时状态为 partial 并写入 warnings,不伪装成 completed 验收:typecheck 通过;241 个测试全绿(含真实 git 仓库端到端)。
阶段 9+10 Finding 收敛与双报告(内核交付里程碑)。 - line-resolver.ts:exact / diff-resolved / unresolved 三级降级;删除行不作为评论位置;证据不足不瞎猜 - dedupe.ts:指纹含位置+类别+归一化证据(标题不参与,避免措辞差异漏合并),保留高置信度那条 - redact.ts:Bearer/URL 凭据/赋值密钥/私钥块/高熵串五类规则,报告自身不再成为泄露载体 - markdown.ts:结论→状态→问题清单→敏感点名→未覆盖→成本→分工→诊断;未完成时醒目警告 - schema.ts:JSON 报告结构与 REPORT_SCHEMA_VERSION(只增不改) - writer.ts:MD+JSON 双写、原子 rename、报告目录必须位于 home 下(issue #23 教训) 验收:typecheck 通过;268 个测试全绿。
端到端演示脚本:临时仓库 + 本地假端点 → 完整编排 → 双报告落盘。 用于人工验证链路,不参与 CI、不联网。
阶段 11 任务簿与内核生命周期。 - store/paths.ts:DSH_HOME 优先的目录布局(修 issue #13 的硬编码问题) - store/atomic.ts:临时文件 + rename 原子写(不会留下半个 JSON) - store/task-store.ts:一任务一文件 + revision 乐观并发;mutate 总是收到磁盘最新记录 - kernel/lock.ts:openSync('wx') 独占创建;nonce 防 pid 复用误判;死进程的锁可回收 - kernel/execute-task.ts:配置取自任务卡快照(中途改设置不影响在跑的任务);端点/凭据缺失在外呼前失败 - kernel/serve.ts:认领 → 执行 → 状态推进;心跳过期判 recovering;空闲退出 验收:typecheck 通过;293 个测试全绿。
阶段 12 插件薄壳。 - plugin/submit.ts:唯一的参数校验点(工具与命令共用);commit 缺 SHA fail-closed;模式互斥 - plugin/process.ts:detached + unref 拉起内核;活着就不拉、死了才拉 - plugin/present.ts:单一呈现点,避免工具/命令文案漂移 - plugin/index.ts:Cordis apply,注册 code_review / code_review_status,可选 /review 命令 - kernel/main.ts:内核 CLI 入口(参数解析 + 依赖组装) 插件层不再 spawn 二进制、不再解析引擎输出,只做「参数 → 任务卡 → 拉起内核」和「读任务卡 → 呈现」。 验收:typecheck 通过;310 个测试全绿。
阶段 13 设置面板(数据层,照搬 dsh-ops 三层的前两层)。 - settings/schema.ts:字段描述(type + meta.volatile + description)+ Standard Schema validate,渲染与校验共用一份描述 - panel/row-schema.ts:GroupSpec/FieldSpec 纯数据页面结构,路径寻址,gatedBy 门控 - patchFor 只产出目标路径的 patch,写回时不会全量覆盖(保护未知字段) - projectGroups 投影当前值;门控关闭时字段置灰但值保留 验收:typecheck 通过;18 个新增测试全绿(含「每个字段路径在默认配置里都有效」的防错字断言)。
阶段 14 控制变量基准。 - scripts/benchmark-tokens.mjs:同一份 system + diff,只改变工具交互方式 - native 模拟 7 轮工具调用(结果进上下文并逐轮重发) - ptc 模拟 1 轮程序执行(只有返回值进上下文) - 默认只估算不联网;--real + BENCH_* 环境变量才真实调用 - 输出明确标注「估算只看量级」,不宣称精确节省 本地估算结果:8480 vs 362 tokens(8 次请求 vs 2 次),量级差异与设计预期一致。
阶段 15 端到端验证。 - 提交(真实参数校验 + 任务卡)→ 内核认领 → 编排(git/diff/筛选/agent)→ 双报告 → 任务卡终态 - 用假 LLM 端点,不联网不花钱,可进 CI - 三条断言链:完整链路产出报告;缺端点时零外呼且任务失败;敏感文件不送审但被点名 全量:26 个测试文件、331 个测试全绿。
阶段 16 退役与收尾。 - 删除 engine/(Go,477 文件 / 6.6MB)与 bin/codecheck.exe;归档于 archive/0.1.3-go-engine 标签 - 删除旧插件外壳 src/*.js(17 个)、client.js、std-facet.js 与 16 个旧冒烟脚本 - package.json 重写:main → dist/plugin/index.js,版本 0.2.0,去掉 schemastery/js-yaml 运行时依赖 - 新增 config/migrate-legacy.ts:首次启动把 plugin-config.json 自动搬到 config.json,旧文件损坏时拒绝迁移 - README 重写为 0.2.0 架构说明;CHANGELOG 增加 0.2.0 条目(含 Known gaps) 验收:typecheck 通过;构建产出 50 个模块且入口可加载;27 个测试文件 / 338 个测试全绿。
补齐 DoD 缺口:任务取消此前只有字段、没有入口。 - requestCancellation:排队中直接转 cancelled(立即生效);执行中只打 cancelRequested 标记 - 关键取舍:标记 ≠ 已取消。把请求当结果会让用户以为线程停了,实际还在烧 token - 内核认领时跳过已请求取消的排队任务并标 cancelled - 已在终态的任务取消会报错而不是静默成功 - code_review_status 增加 cancel 参数作为用户入口 验收:typecheck 通过;28 个测试文件 / 345 个测试全绿。
- verify:类型检查 → 测试 → 构建 → 入口可加载断言 → 基准(估算模式),ubuntu + windows 双平台 - secret-scan:确保没有被跟踪的凭据文件 - 明确红线:需要真实凭据/联网的验证不进 CI - workflows/README 同步更新(引擎已退役,不再有 golangci-lint)
补上设置能力的可用路径(设置面板客户端尚未实现)。 - plugin/set-config.ts:扁平键值对 patch(section.key),逐字段校验 - 未知字段 / 只读字段 / 类型不符 → 拒绝并回显原因,不静默丢弃 - 数值越界钳制而非报错;字符串 trim;枚举白名单 - 只 patch 被点名的路径:同区其他字段与未知顶层字段原样保留 - 损坏的配置文件拒绝写入(不覆盖用户数据) - 注册为 code_review_set_config 工具 验收:typecheck 通过;15 个新增测试全绿。
插件挂载的模拟验收(本地最接近实机的证据)。 - 用模拟 Cordis 上下文调用 apply,断言三个工具与 /review 命令都注册成功 - 注册出来的工具真的能被调用:提交拿到 task_id、status 能查到、set_config 能改并回显 - commands 服务缺失时只告警,不影响工具注册 - 每个用例用独立 DSH_HOME(不污染真实数据) 全量:30 个测试文件、371 个测试全绿。
scan 模式的端到端验证(此前只有 listScanFiles 的单元测试)。 - scan 模式跑通并产出双报告,coverage 覆盖全部源文件 - 依赖目录(node_modules)不进评审:要么显式排除并记录原因,要么根本不进候选 - 与 review 路径共用同一套内核流程,只换取数方式 全量:31 个测试文件、373 个测试全绿。
- scripts/release.mjs:发布前机械校验(工作区干净 / 入口可加载 / 产物无源码与测试 / 许可证齐全)+ 文件清单与 SHA-256 - tsconfig.build.json:关闭 sourcemap,发布包体积减半且不暴露源码结构 - UPGRADING.md:0.1.3 → 0.2.0 的完整升级说明(配置自动迁移、两处行为变化、必须重新配置端点、回退方法、已知限制) - CONTRIBUTING.md:移除已退役引擎的引用 验收:release:check 全部硬性检查通过。
设置面板客户端(用户授权后实现)。 加载机制(按 DSH v0.2.1 源码确认):宿主扫描 dsh.client.platform=web 的包,读 exports["./client"] 指向的单个 bundle 作为浏览器半边。缺 ./client 会直接报错。 - client/index.tsx:注册 plugins.row.config slot(key=dsh-code-review) - client/logic.ts:纯逻辑(控件派生 / 草稿 diff / 快照拉平),无 React 无 DOM,可测 - client/page.tsx:渲染层,只负责把 logic 的结果画出来 - client/types.ts:与宿主的最小契约(ConfigPageForm 形状的显式声明) - settings/fields.ts:字段描述抽成零依赖纯数据,宿主端与浏览器端共用同一份 - scripts/build-client.mjs:esbuild 打包成单文件 ESM,react 与 @deepseek-ai/* 保持 external 顺带修掉一处死代码:domain/validate.ts 此前从未被调用,现在接进了任务卡读取路径(坏卡不再被当成正常卡)。 验收:双端 typecheck 通过;387 个测试全绿;bundle 15.9 KB 且 React 未被内联。
- package-lock.json:同步 ajv 与 react 类型依赖 - tsconfig.json:加 jsx 配置,使测试能解析到 .tsx 模块(只类型检查,不 emit)
- README:三种等价的配置方式(面板 / 工具 / 直接编辑),说明面板只提交改动过的字段 - UPGRADING:已知限制第 1 条从「没有图形面板」改为「面板未经实机验证」
- release.mjs 增加 --pack:用系统 tar 打出 dsh-code-review-<version>.tar.gz - 归档内容:dist + package.json + README + UPGRADING + CHANGELOG + LICENSE + NOTICE + cordis.patch.yml - 归档路径与字节数写回 manifest,便于发布后核对
1) runner 子进程会继承内核的环境变量 Node 的 spawn 默认继承父进程 env,内核持有 API key 时, 模型写的程序一句 process.env 就能读走。改为只给白名单 (PATH/系统根目录/临时目录/编码),凭据类变量一律不传。 2) 运行中的任务不响应取消 此前只在排队阶段检查 cancelRequested,执行中就无视了 —— 用户点了取消,内核还在烧 token,且最终仍报 completed。 现在认领后、执行后各查一次:命中即落成 cancelled。 3) scan 模式盲审 scan 枚举文件清单没有 diff,token 估算恒为 0,被判成小档; 小档既不给 diff 内容也不给工具 —— 模型什么都看不到。 改为 scan 恒按大档处理:主 agent 拿清单 + SDK,正文按需 read。 验收:新增 15 个测试(环境隔离 4、取消流程 6、scan 档位 5);全量 412 个测试绿。
第四个自查发现的问题:行号定位从未真正生效。 执行层调用 resolveLocation(undefined, ...) 拿不到 diff, 于是每条发现都被标成 unresolved —— 报告里全显示「位置未定位」, 哪怕模型给的行号完全正确。 定位必须在同时握有「模型行号」和「真实 diff」的地方做,也就是编排层: - result-parser 给 lineSource 一个乐观默认(exact) - 编排层用真实 FileDiff 复算:命中新增行=exact,证据唯一匹配=diff-resolved(并修正行号),否则 unresolved - 执行层只透传,不再自行判断 验收:新增 3 个测试覆盖三种定位结果;全量 405 个测试绿。
自审查出的两个真 bug。 1. dedupeFindings 写了但从未被调用 —— 主 agent 与子 agent 各自报告同一问题时, 报告里会出现两条一模一样的条目。现在在行号定位之后去重(指纹含行号,顺序不能反)。 2. 所有发现的 sourceAgent 被硬编码成 'main' —— 子 agent 的贡献被冒名。 现在编排层按 agent 标注来源,Finding 增加 sources 数组,去重合并时取并集。 顺带:RawFinding 增加 sourceAgent/sourceTask 字段,来源在编排层确定后一路透传到报告。 验收:typecheck 通过;406 个测试全绿(新增用例让主/子 agent 报同一问题,断言合并为一条且来源都在)。
自审查出的第三个问题:整个报告 redactDeep 会把 commit SHA(40 位十六进制) 当成高熵密钥打成 [REDACTED] —— 用户拿到报告最想核对的就是「审的是哪个提交」, 而那恰恰是唯一被遮住的信息。 改为只脱敏自由文本:findings 的 title/evidence/explanation/recommendation 与 warnings。 结构化标识(commit、路径、任务 id、敏感文件清单)保持原样 —— 它们用于核对,脱敏会破坏可追溯性。 验收:新增测试用「含真实形态密钥的提交」走完整 commit 模式评审, 断言报告中密钥被遮住、SHA 完整保留(JSON 与 Markdown 两份都验)。 全量 407 个测试通过。
- 删除 dsh-plugin.json:0.1.x 的 std-facet 清单,entry 指向已退役的 std-facet.js; DSH 0.2.x 读 package.json 的 dsh 字段,不读这个文件(已核对 DSH 源码) - package.json files 同步:移除 dsh-plugin.json,加入 UPGRADING.md 与 CHANGELOG.md - NOTICE 去掉对已删除文件的引用 - .gitignore 忽略 npm pack 产物
实机前置修复:插件跑在 DSH 的 Electron 进程里,process.execPath 是 Harness.exe 而非 node.exe。 直接用它跑 kernel/main.js 会开一个新窗口而不是执行脚本,症状是「点了评审什么也没发生」。 - 新增 plugin/node-runtime.ts:统一 nodeLaunchEnv(ELECTRON_RUN_AS_NODE=1,对真 node 无害) - plugin/process.ts:拉起内核时带上该环境 - ptc/runner-client.ts:env 白名单保留该开关(否则链路上第二跳又会坏) 验收:414 个测试全绿。
实机崩溃的根因与修复。
现象:安装后 DSH 无法启动,报「web boot: 1 entry did not activate / dsh-code-review: import failed」。
崩溃日志里的 renderer console 给出了直接证据:
Uncaught SyntaxError: Cannot use import statement outside a module
dsh-app://app/plugins/??dsh-code-review/client.js:2
根因:DSH 的 client-modules 把每个插件的 client 产物当**经典脚本**注入,
期望它调用宿主预注入的 window.__ModuleLoader__.load({id, factory}),
依赖(含 React)通过 factory 收到的 require 解析。
我们第一版打成了 ESM,顶层 import 在这种加载方式下非法,
于是整个 web boot 失败——插件坏了会把宿主一起带崩。
修复:
- build-client.mjs 改为 format:'cjs',再用 __ModuleLoader__ 协议包装
- 构建后自检:产物不得含顶层 import/export,且必须调用 __ModuleLoader__.load
- client/index.tsx 去掉未注册的 locale 声明(少一个解析失败点)
对照参考:dsh-ops 的 lib/client.js 正是这个形态(已读源码确认)。
验收:414 个测试全绿;产物 17.7 KB,grep 顶层 import = 0。
实机验收时在 Temp 下发现 178 个残留的 dcr-runner-* 目录。 追查确认不是生产代码泄漏(src/ 里没有任何 mkdtemp),而是 test/ptc/runner.test.ts 建了临时仓库却从未删除 —— 只有 dirs 数组,没有 afterEach。 - 补 afterEach 清理(Windows 上清理失败不阻断测试结论) - 清理历史残留后复跑:414 个测试全绿,Temp 零残留
实机现象:插件装上了、DSH 能启动、bundle 页正常,但「包含的组件」那一行没有配置入口, 说明宿主没看到 plugins.row.config 的注册(ledger.rows 里没有我们的 key)。 宿主那条链路的静默失败点:dsh.client.inject 声明的是「这些包的 factory 必须先到位, 本行才实例化」。列了不需要的包,只会让 apply 永远不执行且不产生任何报错。 本插件并不 import 任何宿主客户端包(组件只用 slot props 传的 form), 唯一依赖的 slots 服务由 Cordis 的 export const inject 等待 —— 所以去掉包依赖。 - package.json: dsh.client 只留 platform + immediately(stage-one 预取) - client/index.tsx: 加两行 console 诊断(模块实例化 / slot 注册成功)与 slots 缺失告警 排查「面板不出现」时,先看开发者工具控制台确认走到哪一步 验收:414 个测试全绿;bundle 18.2 KB;模拟加载器下 apply 注册 key 正确。
The settings panel never appeared because three links were broken at once:
1. plugins.row.config is declared by the plugin-manager entry only while the
plugins page is mounted, so registering at module-eval time threw
`slot "plugins.row.config" is not declared` and was swallowed by try/catch.
Registration now goes through ctx.slots.inject, matching dsh-ops.
2. The host only hands a writable form to rows that expose a config namespace,
and dsh-settings accepts a schema only when `"toJSON" in Config`. We never
exported Config, and the descriptor had no toJSON at all. Every node now
carries a deep-cloning toJSON (the host strips volatile on its copy).
3. The form a row page receives exposes only `state` + `mutate`; `set`/`unset`
are withheld. The page called form.set(), which would have thrown on save.
It now batches settings path ops ({op,path,value}, path as segments) into
one mutate with the host revision.
Configuration now has a single source of truth: the profile patch row. The
plugin reads it from the apply() argument, and code_review_set_config writes
through the host configEditor instead of a private config.json. The kernel is
unchanged - it never read config files, it restores config from the task card.
A stale config.json is only reported, never read or modified.
release.mjs now fails when Config is missing or lacks toJSON.
…to the kernel
Setting review.credentialRef to a correct DSH credential name still failed with
CONFIG_CREDENTIAL_MISSING, for two independent reasons:
1. The kernel only read process.env for the reference name, but DSH keeps
secrets in its own store behind ctx.credentials and never writes them to
the environment (dsh-credentials-local contains no process.env at all).
2. The kernel is a detached long-lived process, so injecting the value through
the spawn environment would pin it until the process restarts - changing a
key would need a full DSH restart.
The host now resolves the reference (DSH store first, environment fallback, the
same order DSH documents), and writes the value into
<dshRoot>/dsh-code-review/kernel/credentials.json. The kernel reads that file per
task before falling back to the environment, so a changed credential applies to
the next review without restarting anything.
Protection level was measured, not assumed: icacls reports the same three ACEs
for this file as for DSH own .credentials.yaml. Node ignores mode 0o600 on
Windows, so the comments and the test now say so instead of claiming 0600.
A malformed reference (dashes, e.g. command-code-api) is reported in the submit
result with an actionable message rather than failing silently later, and the
settings hint spells out the accepted shape.
MSG && git log --oneline -2
The first real review against a live endpoint failed before reaching the model: budget.maxOutputTokens defaults to 0, meaning "unlimited", and that zero was forwarded verbatim as max_tokens. OpenAI-compatible endpoints validate it as a positive number, so the request came back 400 Too small: expected number to be >0 The task then reported "completed, 0 findings" after 0.9s with zero requests, which reads like a clean review instead of a hard failure. "Unlimited" now means the field is absent, not zero. maxTokensFor() does the translation and the loop builds the request with a conditional spread (exactOptionalPropertyTypes rejects an explicit undefined). The fake endpoint in loop.test.ts now records request bodies, and a regression test asserts max_tokens is absent when the budget is 0 and present when it is not. Verified end to end on a live endpoint afterwards: 5 findings over 2 files, 1 request, 1736 in / 2100 out tokens, 16.6s.
…lume A scan of a 1150-line repo reported 0 problems in 22s while both agents hit their round cap. Everything downstream of the diff was blind: scan only listed paths, so estimateFileTokens always returned 0. - scan now synthesizes every supported file as a whole-file addition, so the estimate, the oversized-file filter, the task prompt and the child-split decision all see the real size. Unsupported extensions keep a placeholder so they still land in `excluded` without touching disk. - decideTier no longer forces `large` for scan. That was a patch over the zero estimate; with real numbers the ordinary rule is correct and a small repo now gets its content inline instead of burning rounds on `read`. - default budget.maxAgentRounds 3 -> 8: a three-round cap cannot cover a multi-file review, and hitting it produced "0 problems" instead of a warning. Re-run on the same repo: 0 -> 12 findings (3 high), status completed, 17172 tokens sized instead of 0. Two of the twelve are false positives, which the report now makes checkable because findings carry paths and evidence.
Two defects surfaced by running a real review on an unfamiliar repo:
1. A partial review was presented as "completed, 0 findings". The task stage
only says the kernel finished this pass; the review itself may have hit the
round cap or failed mid-run. The summary now carries status and warnings,
and the task text uses a warning icon plus an explicit "did not finish"
line instead of a check mark. Report schema is unchanged.
2. Small-tier reviews dispatched sub-agents even though the main agent already
received every file inline. Measured on a 2-file, 17k-token repository: five
children were spawned and all five hit max-rounds, burning most of the
tokens without adding a single finding. Children are now dispatched only
when the main agent works from an outline (medium and large tiers), which
is what mainAgentPolicy already promised.
Also in this change:
- scan mode synthesizes whole-file diffs, so token estimates, the oversized
file filter and the task prompt all see real sizes instead of zeros;
- decideTier no longer forces large for scan, since the estimate is real;
- DEFAULT_CONFIG.budget.maxAgentRounds 3 -> 8, matching a multi-file review;
- max_tokens is omitted when the output budget is unlimited (0), because an
OpenAI-compatible endpoint rejects max_tokens: 0 with a 400.
Two defects made the review unreproducible, and together they turned a partial run into a false clean bill of health. 1. The request never set temperature, so every run sampled at the provider default. The same repository, same config, same model gave 12 findings on one run and 0 on the next. A review tool whose answer changes between identical runs is not usable as a gate. 2. Findings only ever come out of the finish tool. When the round budget ran out before the model called finish, the loop returned an empty result that was indistinguishable from "nothing found". The loop now spends one extra request on a salvage turn with finish as the only available tool, so an exhausted budget still yields what was found so far. The stop reason stays max-rounds, so the report still says partial. Measured on the same repo (dsh-zh-layer f0044741), same scan args: before: 204,836 tokens, 477s, 18 requests, 6 agents, 12 findings after: 62,009 tokens, 137s, 2 requests, 1 agent, 13 findings Also: small tiers no longer dispatch child agents. The main agent already receives the full text at that size, so children could only re-read the same files; in practice all five of them hit their own round cap.
…tput Two mechanical failures found while reviewing a third-party plugin on real traffic. Neither is model variance; both are deterministic. 1. The model emits reasoning_content before content. Measured against the configured endpoint with the same prompt: max_tokens=200 -> 698 chars of reasoning, 0 chars of content; 400 -> 1435/0; 800 -> 2643/0. Once the output budget is too small the body is always empty and finish_reason is "length". The loop used to treat that as "model said nothing", nudge twice and end with no-progress, which the report rendered as a clean review with zero findings. It now returns MODEL_RESPONSE_INVALID naming the setting. 2. A finding whose category or severity fell outside our whitelist was dropped outright. The model used category:"info" on a real run and one genuine finding disappeared between the model output and the report. Both enums now coerce (category -> other, severity -> info) and record why. Note on determinism: temperature 0 is sent and the endpoint ignores it -- five identical requests produced five different answers, which is expected for a reasoning model. Repeat runs are samples, not replays; the report already exposes coverage and warnings so a partial run cannot pass as clean.
We only read `prompt_cache_hit_tokens` (DeepSeek native). OpenAI-compatible gateways report the same fact as `prompt_tokens_details.cached_tokens`, and some put `cached_tokens` at the top level. Against such a gateway every report said "cache hits: 0", which reads as "this endpoint has no prefix caching" when in fact we were simply looking in the wrong place. normalizeUsage now takes the first present value across the three shapes, preferring the native field when a provider sends both. Five cases cover the matrix, including "none present" which stays null rather than reporting 0.
budget.maxCostCny was a dead knob: defined in the schema, the defaults and the settings page, but never read by any code. Setting a cost ceiling did nothing, which is exactly the control the original "one review cost 40 yuan" pain needed. Cost accounting is now opt-in by price, as specified: three per-million-token prices (input / output / cache read) plus a currency. When all three stay 0 the whole chain is skipped - priceTableOf returns null, usage.cost stays null, no amount is ever printed and the ceiling never fires. Filling any price turns pricing on and the ceiling starts stopping runs. Details worth keeping: - cached tokens are subtracted from input before pricing, because providers report prompt_tokens inclusive of cache hits and cache is ~10x cheaper; a zero cache price falls back to the input price instead of becoming free. - children receive the remaining budget, not the full ceiling, so one task cannot spend maxCost once per agent. - maxCostCny is still accepted as a legacy alias on read. - null cost means "not measured", never "0 yuan", in the report, the markdown and the status line.
budget.maxCostCny was defined in config, schema and the settings page but never read by any code - the "cost limit" was decoration. Enforce it for real: - budget gains maxCost, currency (CNY/USD), inputPricePerMTok, outputPricePerMTok and cacheReadPricePerMTok. All three prices default to 0, which means "not configured": priceTableOf() returns null, the loop never charges anything, the report prints no amount, and nothing can be interrupted by a cost that was never measured. - domain/cost.ts prices cached tokens separately: providers count them inside prompt_tokens while charging an order of magnitude less, so billing every prompt token at the input price would overcharge. - the loop checks the running cost before each call and stops with stopReason budget; child agents get the remaining balance instead of the full limit. - maxCostCny is still accepted as a legacy alias so nobody loses a saved value. Separately, the numeric inputs could not accept decimals. The field was controlled by a parsed number, so typing "0." collapsed to "0" and swallowed the point. Numeric controls now keep the raw text while editing (inputMode decimal) and parse once on save, rejecting invalid text by field name instead of silently turning it into 0.
The ceiling is checked before each model call, so a single call can exceed it and a one-round review never trips it at all. Proved by setting it to 0.000001 and watching a 0.0092 run finish normally and silently. Option A was chosen: keep the "stop before the next call" semantic rather than guessing a call cost up front and risking a false stop. What changes is that the overshoot is now named in the report instead of passing unnoticed, and the config hint states the semantic instead of implying a hard guarantee.
The overshoot notice rendered a 0.000001 ceiling as "0.00", so the message read "0.01 CNY exceeded the 0.00 ceiling" - technically correct, practically useless. Amounts below one fen now keep up to six significant decimals, and anything still rounded to zero falls back to scientific notation. The report, the status output and the notice share the same formatter.
The first CI run (windows-latest passed, ubuntu-latest failed) proved that `repo_path: 'D:/repo'` is not an absolute path on Linux, so submitReview correctly rejected it with INPUT_REPO_PATH_NOT_ABSOLUTE and five assertions went red. The code was right; the test input was Windows-bound. ABS_REPO / ABS_SHORT now follow the platform, with a comment recording why. Windows suite: 503 passed.
Second CI round: the same Windows-bound input ('D:/some/repo') failed two
apply tests on ubuntu-latest. Everything else on Linux was already green.
A rewrite this deep should not pretend to upgrade in place: the engine changed and the config source moved from a plugin-owned file to the profile row. A half-migrated state is worse than a clean reinstall. Deletes the whole dead chain (migrate.ts / migrate-legacy.ts / store.ts plus their three test files) - none of it had a production caller since config moved to the profile, and leaving it made the tests look like they protected a live path. UPGRADING.md now states "unsupported, uninstall then install" and documents the two legacy files as read-only. The startup notice names both and explains why they are not read. README no longer points at config.json.
Nicholas023
marked this pull request as ready for review
November 4, 2026 15:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概要
把 0.1.x 基于 Go / open-code-review 的引擎整体换成 自研 Node/TypeScript 内核(#37)。
0.1.x 不能升级到 0.2.0,必须卸载后重新安装,并重新填写设置。
原因不是偷懒:引擎换了(不再有
codecheck.exe),配置真源也搬了位置(从插件私有文件搬到 DSH 的 profile 行)。两件结构性变更叠在一起做自动迁移,只会产出半新半旧的怪状态。详见
UPGRADING.md。启动时若检测到旧配置文件,会在日志里点名告知(但不读、不改、不删)。主要变化
codecheck.exe(56 MB)plugin-config.jsonConfig生成设置页schemaVersion)实机验证(不是只有单测)
发版清单(由维护者执行)
mainv0.2.0release/dsh-code-review-0.2.0.tar.gznpm publish(如需要)验收状态
DoD 19 条:15 达成,2 条待改文档措辞,1 条按维护者要求不做(token 基准),1 条只剩发布动作。
本 PR 由 DeepSeek Harness 辅助;CI 具体结论见 checks。