@feng0531/dsh-code-review 0.88.0 → 0.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +15 -1
- package/RELEASE-NOTES.md +10 -12
- package/evaluation/README.md +6 -0
- package/index.mjs +9 -2
- package/package.json +2 -2
- package/src/approved-retrieval.mjs +13 -0
- package/src/background-review.mjs +1 -0
- package/src/child-output-schema.mjs +22 -0
- package/src/counter-verification.mjs +48 -0
- package/src/finding-ledger.mjs +169 -0
- package/src/host-review-children.mjs +100 -0
- package/src/incremental-review.mjs +95 -0
- package/src/project-execution.mjs +6 -1
- package/src/project-review-service.mjs +12 -6
- package/src/report-findings.mjs +5 -0
- package/src/report-summary.mjs +43 -4
- package/src/review-child-broker.mjs +65 -0
- package/src/review-child-plugin.mjs +32 -0
- package/src/review-effort.mjs +104 -0
- package/src/review-history.mjs +29 -0
- package/src/review-jobs.mjs +1 -0
- package/src/review.mjs +10 -6
- package/src/service.mjs +37 -10
- package/src/verification-loop.mjs +3 -0
- package/src/whole-scope-review.mjs +51 -18
- package/src/whole-scope-workers.mjs +88 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
本文件介绍对使用者有意义的变化。安装与参数见 [README](README.md),本次发布正文见 [发布说明](RELEASE-NOTES.md)。历史提交和安装包保留在 [GitHub Releases](https://github.com/f-e-n-g-0531/dsh-code-review/releases);本文不把开发日志当作功能介绍。
|
|
4
4
|
|
|
5
|
+
## 0.90.0 — 清晰结论与有界复审闭环
|
|
6
|
+
|
|
7
|
+
- 数量优先摘要、稳定候选编号与严重程度排序,分开不确定和未完成,保留全部原始证据。
|
|
8
|
+
- 确定性风险投入与高风险候选独立反证,共享额度和新回执,反证失败不再记为完成;不使用多数票替代证据。
|
|
9
|
+
- owned历史台账、人工处置与最多三轮关联修复链;显式差异复审可省未变主审但始终partial,默认未知依赖全新主审,取消成果不丢失。
|
|
10
|
+
- 真实事件评测合同保留全部运行分母,未裁决无金标;r7364归档质量指标未知,不宣称已解决或提速。
|
|
11
|
+
|
|
12
|
+
## 0.89.0 — 超限分组的真实子智能体并行
|
|
13
|
+
|
|
14
|
+
- 整体路径只有完整输入超限分组时创建真实spawn子会话,默认2组、可配1~4;同组持续上下文补读,主审全部收尾后集中复核。默认策略仍staged。
|
|
15
|
+
- 受限快照工具强绑定child身份,共用唯一读取账本和模型生成尝试额度;能力或身份变化拒绝,不隐藏串行回退。逻辑LLM输入检查不冒充最终HTTP字节。
|
|
16
|
+
- FIFO检查点、取消join和首个致命原因保真,按批准顺序汇总;跨组未核查仍partial。
|
|
17
|
+
- 实际宿主独立离线Context通过两个真实child的并行、持续读取和结构化终态验证;模型、快照和cwd为fixture,不宣称真实提供方质量或业务提速。
|
|
18
|
+
|
|
5
19
|
## 0.88.0 — 整体主审实验路径与自动回归
|
|
6
20
|
|
|
7
21
|
- 新增whole-scope完整文件字节合批与候选集中复核,显式输入容量绑定预览和执行;默认仍staged,fresh单轮changes限定,超大文件及跨批缺口如实保留。
|
package/README.md
CHANGED
|
@@ -280,10 +280,24 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
|
|
|
280
280
|
|
|
281
281
|
插件配置 `executionStrategy: "whole-scope"` 启用完整文件按字节合批的变更审查。能装下时,一次主审同时检查全部成员;超限才分批,不按四个文件强切,不再额外调用逐文件风险规划、业务分组及旧专门综合。候选集中独立复核,缺少上下文时仍可能增加检索调用,因此不承诺总调用仅一次。
|
|
282
282
|
|
|
283
|
-
`
|
|
283
|
+
实际超限分为多组时,整体路径使用宿主真实 `spawn` 子智能体有界并行,`wholeScopeConcurrency` 可设1~4(默认2);只有一组时不创建子智能体。每组保持独立会话和持续上下文,补读不重新建组;全部主审收尾后集中复核。宿主须具备受限预设、结构化子会话和活动父Agent;能力缺失或变化明确拒绝,不暗中退回串行。子智能体共用唯一批准快照读取账本与模型生成尝试总账,取消须等待所有子会话清理。跨组关系未核查仍partial,并行不提高完成标准。
|
|
284
|
+
|
|
285
|
+
内部 `review_snapshot` 工具可能在父会话目录可见,但只有本次注册且身份绑定的审查子会话可执行;父会话和其他子会话读取前拒绝。子会话作为宿主管理的独立Session按宿主规则记录,可能含批准源码和工具响应,不等同于插件私有模型工件留存。
|
|
286
|
+
|
|
287
|
+
`maxInputBytes` 是插件显式输入保护,不是模型上下文容量探测;默认仍为 98304 字节,允许配置为 1~4194304 的整数。主审计划计入完整新旧正文、JSON转义、规则、检索目录、指令与原生结果工具;持续子会话发送前另外检查包含历史消息和工具的逻辑LLM envelope,不是提供方最终HTTP字节。宿主系统提示与历史开销可能使已规划组在发送前受限,届时保留未完成而不截断源码。预览的 `plan.maxInputBytes` 与执行使用同一值。提高前应核实当前模型容量、费用及提供方限制,插件不会自动猜测容量或因失败不断加大输入重试。
|
|
284
288
|
|
|
285
289
|
当前仅支持 fresh 单轮 changes;多轮、旧任务恢复及 project 明确拒绝,预览显示 `resumeSupported:false`。超大完整文件显示 blocked,不截断或把差异窗口冒充全文;多批跨边界未核查保持 partial。身份、原文定位、归因、独立复核回执及来源新鲜度校验不变。默认仍为 `staged`;整体路径尚无真实业务同条件质量与耗时验收,不宜把单测调用减少当作业务问题已解决。配置由宿主插件管理方式加载,发布不会自动安装或重启宿主。
|
|
286
290
|
|
|
291
|
+
## 风险投入、发现台账与复审
|
|
292
|
+
|
|
293
|
+
`reviewEffort` 可选 `auto`(默认)、`quick`、`standard`、`deep`,在preview中明确。它是对批准变更的有界确定性检查,不额外调用模型规划;quick也不跳过原文、归因和独立复核。高风险线索只改变检查方向,不证明缺陷。深度模式下仅已获首轮支持的high/critical候选增加一次独立反证复核,使用新的读取回执;不把首轮结论传给第二位验证者。两次结论分歧、证据不足或反证未完成均保守处理,不按多数票升级。插件配置 `enableCounterVerification:false` 可关闭额外反证;开启状态在preview绑定。它可能增加调用和耗时,不承诺普遍提速。
|
|
294
|
+
|
|
295
|
+
变更审查可在新后台preview传 `rereviewFromJobId` 接续本会话、相同仓库/模型/规则/需求政策的终态且未过期报告。每轮仍重新捕获、预览和授权;旧回执及结论不进入本轮证据。关联修复链第三轮提示重新考虑方案,第四轮拒绝;不是跨任务全局次数锁。可用 `findingOutcomes` 记录已知发现的 `fixed/refuted/intentional/deferred` 与原因,人工标记是处置声明,不证明修复。相同精确定位与缺陷主张保持台账身份,不因行号移动重复建项;未再次观察不自动关闭。
|
|
296
|
+
|
|
297
|
+
默认 `rereviewScope:"full"`,未知完整依赖图时保持全新主审。明确选择 `rereviewScope:"diff-only"` 才将新旧两侧、属性和来源指纹都未变的主文件保留为未重审;本轮完整批准来源仍可检索。预览列出primary与unreviewed路径,实际省略未变主文件时报告强制partial,不能当作跨文件影响已排除或全范围通过。取消/失败终态不被partial覆盖。上述修复链和差异复审仅changes支持;project支持风险投入但不接受历史修复链参数。
|
|
298
|
+
|
|
299
|
+
报告首屏先给复核支持数量和优先候选编号,分开已复核不确定与复核未完成,显示未查范围。正文按类别与严重程度展示,附录保留原候选顺序与完整证据。真实事件评测见[仓库评测说明](https://github.com/f-e-n-g-0531/dsh-code-review/blob/main/evaluation/README.md);没有专家确认和完整实际结果时不输出命中率。
|
|
300
|
+
|
|
287
301
|
## 工具调用流程(给需要精确指定参数的人)
|
|
288
302
|
|
|
289
303
|
普通使用者只需上面的对话请求,不必手工构造 ID。需要精确控制时,让 Agent 按下列顺序调用:
|
package/RELEASE-NOTES.md
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
# v0.
|
|
1
|
+
# v0.90.0 发布说明
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 报告、收敛复审、风险投入与真实事件评测统一优化
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- 结论首屏按数量说明复核支持的新增/既有/当前源码问题,分开已复核不确定和复核未完成,优先列出严重候选编号与未查范围;不把空发现等同通过。原始候选及证据保持不变。
|
|
6
|
+
- 新增确定性风险投入auto/quick/standard/deep,不增加独立模型规划;quick保留严格校验。深度模式仅对首轮已支持的high/critical候选增加新回执的独立反证,不给第二验证者首轮结论;分歧保守不确定,反证失败明确incomplete,不缓存成完成。反证共用原预算、取消和来源边界,可能增加时间,不承诺提速。
|
|
7
|
+
- 变更审查新增owned终态报告修复链,fresh preview与授权不可省。稳定指纹合并重复定位和缺陷主张,人工fixed/refuted/intentional/deferred仅记录处置,不证明当前状态;未再次观察不自动fixed。关联链第三轮要求step-back,第四轮拒绝,不是全局次数锁。
|
|
8
|
+
- 默认全新主审。显式diff-only复审才减少完全未变文件的主审目标,批准原源仍可检索,跳过范围明确未重审,实际省略主文件时报告保持partial;未知依赖不得自动推断完整覆盖。不构造虚假历史差异,不继承旧回执或授权。取消与失败终态保留。修复链仅changes支持,project支持风险投入。
|
|
9
|
+
- 新增离线真实事件评测合同:固定快照/版本/模型及预注册全部运行,专家裁决绑定实际报告摘要哈希和候选序号;未裁决不作金标,缺失/失败/partial不从分母消失。分别统计误报、有害建议、未完成和操作耗时,不以最佳一次或模型自评作为成绩。
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
## 验证与未完成边界
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
本轮有合同、生产服务夹具、取消/身份/预算/定位回归与实际宿主独立离线子会话兼容验收;离线子会话只验证transport,不代表新增风险或反证管线真实模型成绩。r7364公开归档确证旧0.87.9为156次调用、861.351秒及大量未完成,但缺少完整原report JSON与专家确认标签,评测保持unadjudicated、质量指标null,不重建假报告或混用新鲜快照。没有同条件真实模型质量/耗时改善结论。
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
用户r7364归档实际为0.87.9:74条目中48完成、4失败、1阻断、21待审,综合3/33,156次模型调用,执行861.351秒。四项关键主审原文定位失败,且全局读取仍有余量;此前小样例不能代表该业务闭环。
|
|
14
|
-
|
|
15
|
-
同修订新鲜快照完整新旧源码约1.96MB。静态规划在96KiB下17批且15完整文件超限;256KiB下13批、1MiB下3批、3MiB下1批,后三者均装入73个可审条目。这些只是输入规划,不是模型可接收能力、实际调用数或质量提升证明;新鲜快照不冒充归档原身份。
|
|
16
|
-
|
|
17
|
-
本版通过离线合同测试及宿主工具兼容验收,尚无新策略真实模型同条件质量与耗时成绩,**不宣称r7364已解决或已提速**。按用户要求正式发布实验能力,默认不切换,不自动安装、改配置或重启运行宿主。配置见[使用说明](README.md),正式发布门禁见[开发说明](DEVELOPMENT.md)。
|
|
15
|
+
本版保持只读,不自动修改/合并业务代码,不替代所需人类审批,不自动安装、改宿主配置或重启。配置见[使用说明](README.md),正式门禁见[开发说明](DEVELOPMENT.md),设计借鉴[Anthropic文章](https://claude.dev/blog/solving-code-review-at-anthropic/)。
|
package/evaluation/README.md
CHANGED
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
第二条命令只读取固定合成输入,校验内容指纹并计算初始输入大小,不调用模型。输出不含源码或答案标签;它也不是宿主执行授权。
|
|
19
19
|
|
|
20
|
+
## 真实事件评测合同
|
|
21
|
+
|
|
22
|
+
[incident-eval.mjs](incident-eval.mjs)校验并评估外部注入的完整实际report与专家裁决,[schema](incidents.schema.json)定义固定快照/版本/模型身份、预注册全部runIds和confirmed/unadjudicated。confirmed中的reviewer与judgementRef是外部裁决声明,不是加密签名;评测器不自动确认专家身份或从模型判定推导金标。候选裁决必须绑定report精确JSON SHA256与索引。缺失、失败和partial保留所有预注册分母;未裁决或无金标时precision/recall为null,不以0冒充无缺陷。误报、有害建议、已处置与未完成分别统计。
|
|
23
|
+
|
|
24
|
+
运行`node --test test/incident-eval.test.mjs`仅验证记账合同,不是业务成绩。真实业务素材、答案和原始会话保持仓库外,不进入安装包;模型输入不包含金标。当前r7364归档只验证旧调用和状态观察,缺完整report JSON与专家裁决,因此不宣称实际召回率或新版改进。任何prompt/工具/模型变化应重新运行同一组全部案例,不选最佳一次。可由现有registered-tool live runner保存新鲜预览和终态回执,再人工独立裁决;不直连provider、不自动读取私有模型工件。
|
|
25
|
+
|
|
20
26
|
## 真实模型观察
|
|
21
27
|
|
|
22
28
|
已有六次小规模观察见 [试跑结果](PILOT-RESULTS.md)。其中一次证据复核未完成,其他结果也不能外推成质量指标。新评测仍后置,开始条件见 [运行条件](PILOT-GATE.md)。
|
package/index.mjs
CHANGED
|
@@ -2,19 +2,26 @@ import { registerBackgroundReview } from './src/background-review.mjs';
|
|
|
2
2
|
import { createReviewService } from './src/service.mjs';
|
|
3
3
|
import { DEFAULT_PROJECT_CONCURRENCY } from './src/project-execution.mjs';
|
|
4
4
|
import { reviewToolOutput } from './src/tool-view.mjs';
|
|
5
|
+
import { createHostReviewChildren } from './src/host-review-children.mjs';
|
|
6
|
+
import { attachReviewChildMiddleware } from './src/review-child-broker.mjs';
|
|
7
|
+
import { registerSnapshotTool } from './src/review-child-plugin.mjs';
|
|
5
8
|
|
|
6
9
|
export const name = 'dsh-code-review';
|
|
7
10
|
export const inject = ['tools', 'llm'];
|
|
8
11
|
|
|
9
12
|
const PREVIEW_DESCRIPTION = 'Preview Git/SVN working-copy or Git commit/range review scope and model destination without sending source to a model. repositoryPath selects an explicit child repository relative to session workspace; file paths are repository-relative. Each session reviews one selected repository independently; reviewMode=project selects tracked current source instead of changes; changeScope=staged or unstaged narrows a Git working-copy review to the index or to unstaged edits. Always capture and inspect the preview internally before execution. An explicit user request to review the selected code already authorizes execution within that request: proceed without asking the user to confirm the same review again. Ask only if the preview exceeds the requested scope or changes the authorized model/destination, or authorization is unclear. Never execute if the user requested preview only or explicitly asked to wait for confirmation. Preserve host tool permissions and all snapshot/identity checks.';
|
|
10
|
-
const PREVIEW_PARAMETERS = { type: 'object', additionalProperties: false, properties: { resumeFromJobId: { type: 'string', description: 'Explicit terminal job to reuse identity-matching completed chains. Requires a fresh captured preview, confirmation and authorization; does not restore old approval.' }, reviewMode: {type:'string',enum:['changes','project'],description:'project reviews all tracked current files, including unchanged files, in bounded batches with shared session budget. Does not include ignored/untracked/external contents. Each DSH session owns independent preview, lock and model budget.'}, changeScope: {type:'string',enum:['worktree','staged','unstaged'],description:'Git working-copy comparison scope. worktree (default) compares HEAD to the working tree (net change); staged compares HEAD to the index so only staged content is reviewed; unstaged compares the index to the working tree so only unstaged edits are reviewed and staged content is the old side. Requires a Git working copy; mutually exclusive with Git history, SVN parameters and project mode.'}, svnReplacementBaseRevision: {type:'string',description:'Working-copy file replacement only: explicitly compare to this fixed repository revision at the target URL. Not local BASE and not proof of the removed-node revision. Requires separate remote opt-in when applicable; identity and properties frozen in preview.'}, svnReplacementAllowNetwork: {type:'boolean',description:'Explicit permission to read the selected working-copy target URL at svnReplacementBaseRevision. Does not permit arbitrary URLs, updates, merges or writes.'}, svnBasePath: {type:'string',description:'Explicit old endpoint path relative to the current SVN repository root; requires svnTargetPath and fixed revision range. Not an arbitrary URL. Same repository UUID required.'}, svnTargetPath: {type:'string',description:'Explicit new endpoint repository-relative path paired with svnBasePath. Each side content/context stays at its URL/revision; shared capture budgets and remote opt-in apply.'}, svnEffectiveMergeinfo: { type: 'boolean', description: 'Explicit SVN opt-in to bounded BASE/working or fixed-history ancestor mergeinfo property reads on each selected side. Offline for working copies; history uses existing remote permission and budgets. Mixed BASE revisions, unsafe ancestors or incomplete URL boundaries remain unknown. No branch source reads or merge attribution.' }, svnRevision: { type: 'string', description: 'SVN fixed numeric revision reviewed against revision minus one. Requires SVN working-copy root; no HEAD/date aliases.' }, svnBaseRevision: { type: 'string', description: 'SVN fixed numeric old endpoint; requires svnTargetRevision, mutually exclusive with svnRevision and Git revision parameters.' }, svnTargetRevision: { type: 'string', description: 'SVN fixed numeric new endpoint, greater than old endpoint. Fixed repository objects, not working-copy text. Supports explicitly selected target context with verified baseline provenance and target rules; explicit old-only context is also supported with fixed SVN provenance; autoContext follows at most three independent fixed-side dependency layers with bounded parent listings and shared capture budgets; callerScopePaths supports bounded fixed-side SVN historical literal discovery; only matched sides captured, never complete caller coverage.' }, svnAllowNetwork: { type: 'boolean', description: 'Explicit opt-in to read the working-copy repository URL at fixed SVN revisions during preview/execute; preview sends no source to a model. Required for non-file repository URLs; default denies remote access. Does not allow checkout/update/merge or arbitrary URLs.' }, reviewRounds: { type: 'integer', minimum: 1, maximum: 3, description: 'Explicit independent review passes, default 1. Preview discloses extra sends; later passes omit the first risk plan. Shares existing timeout, call and retrieval budgets, not provider reasoning effort. Bound to this preview; execute cannot override.' }, oldContextPaths: { type: 'array', maxItems: 20, items: { type: 'string' }, description: 'Explicit Git/SVN historical baseline-only context paths. Git uses fixed blobs; SVN uses UUID/URL/peg/operative revision/hash. Shares 20 context slots. No target text is inferred; selected old changes reuse original identity, excluded changes cannot reenter. Unsupported on working reviews.' }, callerScopePaths: { type: 'array', minItems: 1, maxItems: 4, items: { type: 'string' }, description: 'Explicit non-root repository-relative directories for bounded tracked literal reverse import/include discovery. Git/SVN working review or fixed Git/SVN historical review. SVN uses offline normal nodes, rejects unsafe ancestor states and special/keyword properties. Historical sides share scan budgets and use object blobs only. Preview reads candidate bodies, including nonmatches; only captured matching context is sent. Navigation only, not complete caller coverage.' }, businessRequirement: { type: 'string', maxLength: 16384, description: 'Optional business constraint text up to 16KiB UTF8; untrusted data, not evidence or permission. Bound to preview and sent in review phases.' }, commit: { type: 'string', description: 'Git commit versus sole parent; root empty, merge requires range.' }, baseRevision: { type: 'string', description: 'Git old endpoint; requires targetRevision, mutually exclusive with commit.' }, targetRevision: { type: 'string', description: 'Git new endpoint, exact tree endpoints not merge-base. Explicit context uses target tree with available baseline provenance; oldContextPaths is baseline-only. Historical caller discovery scans fixed sides; explicit rules use target-tree blobs.' }, repositoryPath: { type: 'string', description: 'Workspace-relative explicit child Git/SVN repository root, forward slashes; omit for current repository.' }, selectedPaths: { type: 'array', items: { type: 'string' }, maxItems: 200 }, autoContext: { type: 'boolean', description: 'Opt in to tracked literal import/include context shown in preview. Working Git follows changed and explicitly selected context sources at most 3 dependency layers under shared 512 exact probes, 20 candidates/context slots, 1MiB source and 30s verification budget. Git history follows at most 3 baseline/target dependency layers with shared limits, object reads only; SVN follows at most 3 safe offline current-side layers with exact path probes and node/ancestor identity verification, sharing limits; no SVN historical review. Not semantic binding or complete definitions.' }, contextPaths: { type: 'array', items: { type: 'string' }, maxItems: 20 }, rulePaths: { type: 'array', items: { type: 'string' }, maxItems: 4 } } };
|
|
13
|
+
const PREVIEW_PARAMETERS = { type: 'object', additionalProperties: false, properties: { rereviewScope:{type:'string',enum:['full','diff-only'],description:'Changes only: explicit diff-only re-review omits unchanged primary files but retains approved retrieval context; unknown cross-file impact remains partial, never reuse old evidence. Requires rereviewFromJobId and fresh preview approval.'}, reviewEffort: {type:'string',enum:['auto','quick','standard','deep'],description:'Deterministic review effort policy; every tier retains strict evidence checks. No automatic human approval.'}, rereviewFromJobId:{type:'string',description:'Owned terminal review history for a fresh review-fix cycle; requires fresh snapshot preview and approval, never inherits old evidence or approval. Maximum three linked cycles.'}, findingOutcomes:{type:'array',maxItems:200,items:{type:'object',additionalProperties:false,properties:{id:{type:'string'},decision:{type:'string',enum:['fixed','refuted','intentional','deferred']},reason:{type:'string'}},required:['id','decision','reason']},description:'Explicit human dispositions of known historical findings; claims only, not proof of a fix. Requires rereviewFromJobId.'}, resumeFromJobId: { type: 'string', description: 'Explicit terminal job to reuse identity-matching completed chains. Requires a fresh captured preview, confirmation and authorization; does not restore old approval.' }, reviewMode: {type:'string',enum:['changes','project'],description:'project reviews all tracked current files, including unchanged files, in bounded batches with shared session budget. Does not include ignored/untracked/external contents. Each DSH session owns independent preview, lock and model budget.'}, changeScope: {type:'string',enum:['worktree','staged','unstaged'],description:'Git working-copy comparison scope. worktree (default) compares HEAD to the working tree (net change); staged compares HEAD to the index so only staged content is reviewed; unstaged compares the index to the working tree so only unstaged edits are reviewed and staged content is the old side. Requires a Git working copy; mutually exclusive with Git history, SVN parameters and project mode.'}, svnReplacementBaseRevision: {type:'string',description:'Working-copy file replacement only: explicitly compare to this fixed repository revision at the target URL. Not local BASE and not proof of the removed-node revision. Requires separate remote opt-in when applicable; identity and properties frozen in preview.'}, svnReplacementAllowNetwork: {type:'boolean',description:'Explicit permission to read the selected working-copy target URL at svnReplacementBaseRevision. Does not permit arbitrary URLs, updates, merges or writes.'}, svnBasePath: {type:'string',description:'Explicit old endpoint path relative to the current SVN repository root; requires svnTargetPath and fixed revision range. Not an arbitrary URL. Same repository UUID required.'}, svnTargetPath: {type:'string',description:'Explicit new endpoint repository-relative path paired with svnBasePath. Each side content/context stays at its URL/revision; shared capture budgets and remote opt-in apply.'}, svnEffectiveMergeinfo: { type: 'boolean', description: 'Explicit SVN opt-in to bounded BASE/working or fixed-history ancestor mergeinfo property reads on each selected side. Offline for working copies; history uses existing remote permission and budgets. Mixed BASE revisions, unsafe ancestors or incomplete URL boundaries remain unknown. No branch source reads or merge attribution.' }, svnRevision: { type: 'string', description: 'SVN fixed numeric revision reviewed against revision minus one. Requires SVN working-copy root; no HEAD/date aliases.' }, svnBaseRevision: { type: 'string', description: 'SVN fixed numeric old endpoint; requires svnTargetRevision, mutually exclusive with svnRevision and Git revision parameters.' }, svnTargetRevision: { type: 'string', description: 'SVN fixed numeric new endpoint, greater than old endpoint. Fixed repository objects, not working-copy text. Supports explicitly selected target context with verified baseline provenance and target rules; explicit old-only context is also supported with fixed SVN provenance; autoContext follows at most three independent fixed-side dependency layers with bounded parent listings and shared capture budgets; callerScopePaths supports bounded fixed-side SVN historical literal discovery; only matched sides captured, never complete caller coverage.' }, svnAllowNetwork: { type: 'boolean', description: 'Explicit opt-in to read the working-copy repository URL at fixed SVN revisions during preview/execute; preview sends no source to a model. Required for non-file repository URLs; default denies remote access. Does not allow checkout/update/merge or arbitrary URLs.' }, reviewRounds: { type: 'integer', minimum: 1, maximum: 3, description: 'Explicit independent review passes, default 1. Preview discloses extra sends; later passes omit the first risk plan. Shares existing timeout, call and retrieval budgets, not provider reasoning effort. Bound to this preview; execute cannot override.' }, oldContextPaths: { type: 'array', maxItems: 20, items: { type: 'string' }, description: 'Explicit Git/SVN historical baseline-only context paths. Git uses fixed blobs; SVN uses UUID/URL/peg/operative revision/hash. Shares 20 context slots. No target text is inferred; selected old changes reuse original identity, excluded changes cannot reenter. Unsupported on working reviews.' }, callerScopePaths: { type: 'array', minItems: 1, maxItems: 4, items: { type: 'string' }, description: 'Explicit non-root repository-relative directories for bounded tracked literal reverse import/include discovery. Git/SVN working review or fixed Git/SVN historical review. SVN uses offline normal nodes, rejects unsafe ancestor states and special/keyword properties. Historical sides share scan budgets and use object blobs only. Preview reads candidate bodies, including nonmatches; only captured matching context is sent. Navigation only, not complete caller coverage.' }, businessRequirement: { type: 'string', maxLength: 16384, description: 'Optional business constraint text up to 16KiB UTF8; untrusted data, not evidence or permission. Bound to preview and sent in review phases.' }, commit: { type: 'string', description: 'Git commit versus sole parent; root empty, merge requires range.' }, baseRevision: { type: 'string', description: 'Git old endpoint; requires targetRevision, mutually exclusive with commit.' }, targetRevision: { type: 'string', description: 'Git new endpoint, exact tree endpoints not merge-base. Explicit context uses target tree with available baseline provenance; oldContextPaths is baseline-only. Historical caller discovery scans fixed sides; explicit rules use target-tree blobs.' }, repositoryPath: { type: 'string', description: 'Workspace-relative explicit child Git/SVN repository root, forward slashes; omit for current repository.' }, selectedPaths: { type: 'array', items: { type: 'string' }, maxItems: 200 }, autoContext: { type: 'boolean', description: 'Opt in to tracked literal import/include context shown in preview. Working Git follows changed and explicitly selected context sources at most 3 dependency layers under shared 512 exact probes, 20 candidates/context slots, 1MiB source and 30s verification budget. Git history follows at most 3 baseline/target dependency layers with shared limits, object reads only; SVN follows at most 3 safe offline current-side layers with exact path probes and node/ancestor identity verification, sharing limits; no SVN historical review. Not semantic binding or complete definitions.' }, contextPaths: { type: 'array', items: { type: 'string' }, maxItems: 20 }, rulePaths: { type: 'array', items: { type: 'string' }, maxItems: 4 } } };
|
|
11
14
|
const EXECUTE_DESCRIPTION = 'Execute a previewed read-only code review using the current DSH model. Sends the approved snapshot to the current Agent model under DSH tool execution policy; no separate plugin approval prompt. confirmed=true attests that the current user explicitly requested this review and the inspected preview matches that authorization; it does not require a second confirmation message. Never execute a preview-only or explicit wait-for-confirmation request, or exceed authorized scope/model/destination. Never modifies the reviewed repository.';
|
|
12
15
|
const EXECUTE_PARAMETERS = { type: 'object', additionalProperties: false, properties: { previewId: { type: 'string' }, confirmed: { type: 'boolean', description: 'True only when the current user explicitly authorized this review and the inspected preview matches the requested scope/model/destination. The original explicit review request is sufficient; do not ask again for the same review. Preview-only or explicit wait-for-confirmation requests do not authorize execution.' } }, required: ['previewId', 'confirmed'] };
|
|
13
16
|
|
|
14
17
|
export function apply(ctx, config = {}) {
|
|
15
18
|
// This entry runs only when the host invokes the registered tool.
|
|
16
19
|
// Host tool policy owns permission; do not request a second approval here.
|
|
17
|
-
const
|
|
20
|
+
const hostChildren = createHostReviewChildren(ctx);
|
|
21
|
+
registerSnapshotTool(ctx);
|
|
22
|
+
attachReviewChildMiddleware(ctx);
|
|
23
|
+
ctx.on('dispose', () => hostChildren.dispose());
|
|
24
|
+
const service = createReviewService(ctx.llm, { reviewEffort: config.reviewEffort ?? 'auto', enableCounterVerification: config.enableCounterVerification ?? true, hostChildren, wholeScopeConcurrency: config.wholeScopeConcurrency ?? 2, executionStrategy: config.executionStrategy ?? 'staged', maxInputBytes: config.maxInputBytes ?? 96 * 1024, allowModelSending: true, nativeOutputTools: true, enableGroupPrimary: true, enableEvidenceContext: config.enableEvidenceContext ?? true, inlineInteractionRisks: config.inlineInteractionRisks ?? true, enableMigrationContracts: config.enableMigrationContracts ?? true, enableMigrationRegression: config.enableMigrationRegression ?? true, enableGroupInteractions: config.enableGroupInteractions ?? true, enableStageContinuation: config.enableStageContinuation ?? true, enableRiskPlanning: true, enableBusinessGrouping: true, enableAnchorCorrection: true, projectConcurrency: config.projectConcurrency ?? DEFAULT_PROJECT_CONCURRENCY, riskPlanningMode: config.riskPlanningMode ?? 'auto', readerBudgetPolicy: config.readerBudgetPolicy ?? 'scaled', optimizeReviewCalls: true, enablePrimarySeeds: true, focusPrimaryContext: config.focusPrimaryContext ?? true, enableSearchSeeds: config.enableSearchSeeds ?? true, changesConcurrency: config.changesConcurrency ?? 2, maxRetrievalRounds: config.maxRetrievalRounds ?? 3, authorize: async () => true });
|
|
18
25
|
ctx.tools.register({
|
|
19
26
|
name: 'code_review_preview', description: PREVIEW_DESCRIPTION, parameters: PREVIEW_PARAMETERS, output: reviewToolOutput,
|
|
20
27
|
async execute(args, exec) { const preview = await service.preview(args, exec); return { json: JSON.stringify(preview), markdown: '' }; },
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feng0531/dsh-code-review",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.90.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
|
|
7
7
|
"main": "./index.mjs",
|
|
8
|
-
"exports": { ".": "./index.mjs", "./package.json": "./package.json", "./locale/*.json": "./locale/*.json" },
|
|
8
|
+
"exports": { ".": "./index.mjs", "./package.json": "./package.json", "./child": "./src/review-child-plugin.mjs", "./locale/*.json": "./locale/*.json" },
|
|
9
9
|
"files": ["scripts/review-bench-adapter.mjs", "evaluation/REVIEWBENCH-LOCAL.md", "index.mjs", "cordis.patch.yml", "src/", "locale/", "README.md", "CHANGELOG.md", "RELEASE-NOTES.md", "DEVELOPMENT.md", "CORE-ALIGNMENT-TASKS.md", "CORE-SOURCE-INVENTORY.md", "SVN-SUPPORT-MATRIX.md", "SVN-RELEASE-EVIDENCE.md", "LICENSE", "LICENSES/"],
|
|
10
10
|
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
|
|
11
11
|
"repository": { "type": "git", "url": "https://github.com/f-e-n-g-0531/dsh-code-review.git" },
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
// Internal scope projection only; never an extra public source authority.
|
|
3
|
+
export function approvedRetrievalSnapshot(review,approved=review) {
|
|
4
|
+
if(approved===review)return review;
|
|
5
|
+
if(!approved||review.id!==approved.id||!Array.isArray(review.files)||review.files.length!==approved.files?.length)throw new Error('Incremental source identity mismatch');
|
|
6
|
+
const expected=structuredClone(approved);
|
|
7
|
+
for(let i=0;i<review.files.length;i++){
|
|
8
|
+
const target=review.files[i],source=expected.files[i];
|
|
9
|
+
if(target.eligibility==='excluded'&&target.reason==='incremental-unchanged-not-reviewed'&&source.eligibility==='reviewable'){source.eligibility=target.eligibility;source.reason=target.reason;}
|
|
10
|
+
}
|
|
11
|
+
if(!isDeepStrictEqual(expected,review))throw new Error('Incremental projection changed approved source or metadata');
|
|
12
|
+
return structuredClone(approved);
|
|
13
|
+
}
|
|
@@ -83,6 +83,7 @@ async function startBackgroundJob(args, exec, jobs, service) {
|
|
|
83
83
|
const detached = {
|
|
84
84
|
...exec, agent: original, signal,
|
|
85
85
|
resolveReviewResume: id => jobs.resume(id, exec),
|
|
86
|
+
resolvePriorReview: id => jobs.priorReview(id, exec),
|
|
86
87
|
reviewResumeCheckpoint: saveResumeState,
|
|
87
88
|
onModelArtifact: artifact,
|
|
88
89
|
reviewCheckpoint: async value => {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// The host's structured_output accepts a documented finite JSON Schema subset.
|
|
2
|
+
// Numeric/length bounds remain mandatory in the ordinary review validators.
|
|
3
|
+
const annotations = new Set(['title', 'description', 'default']);
|
|
4
|
+
const scalars = new Set(['type', 'enum', 'const', 'required', 'additionalProperties', ...annotations]);
|
|
5
|
+
export function childOutputSchema(schema) {
|
|
6
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) throw new Error('Invalid child output schema');
|
|
7
|
+
const result = {};
|
|
8
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
9
|
+
if (key === 'properties') result.properties = Object.fromEntries(Object.entries(value).map(([name, child]) => [name, childOutputSchema(child)]));
|
|
10
|
+
else if (key === 'items') result.items = childOutputSchema(value);
|
|
11
|
+
else if (key === 'oneOf') result.oneOf = value.map(childOutputSchema);
|
|
12
|
+
else if (scalars.has(key)) result[key] = structuredClone(value);
|
|
13
|
+
else if (!['minimum','maximum','minItems','maxItems','minLength','maxLength','pattern'].includes(key)) throw new Error('Unknown child schema constraint: ' + key);
|
|
14
|
+
}
|
|
15
|
+
if (!result.type && !result.oneOf) {
|
|
16
|
+
const values = result.enum ?? (Object.hasOwn(result, 'const') ? [result.const] : []);
|
|
17
|
+
const types = [...new Set(values.map(value => value === null ? 'null' : typeof value))];
|
|
18
|
+
if (types.length === 1 && ['string','number','boolean','null'].includes(types[0])) result.type = types[0];
|
|
19
|
+
else if (values.length) throw new Error('Ambiguous child enum type');
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { verificationLoop } from './verification-loop.mjs';
|
|
2
|
+
|
|
3
|
+
const confirmed = value => value?.verdict === 'supported' && Array.isArray(value.evidence) && value.evidence.length > 0 && value.evidence.every(ref => ref?.status === 'references-validated');
|
|
4
|
+
const classification = value => value?.regression?.classification;
|
|
5
|
+
const comparable = value => ['introduced', 'preexisting'].includes(classification(value)) && value.regression.status === 'comparison-references-validated';
|
|
6
|
+
const fatal = (error, options) => ['MODEL_ARTIFACT_FAILED', 'CHECKPOINT_FAILED', 'RESUME_CHECKPOINT_FAILED'].includes(error?.code) || options.isFatalError?.(error) === true;
|
|
7
|
+
|
|
8
|
+
// Input baseline must be the just-completed strict verificationLoop result, not
|
|
9
|
+
// historical findings or model-supplied verdicts. This pass grants no authority.
|
|
10
|
+
export async function runCounterVerification(model, request, candidates, baseline, scope, options = {}) {
|
|
11
|
+
request.signal?.throwIfAborted();
|
|
12
|
+
if (!Array.isArray(candidates) || candidates.length > 50 || !Array.isArray(baseline) || baseline.length !== candidates.length) throw new Error('Invalid counterverification candidates or baseline');
|
|
13
|
+
if (options.enabled !== undefined && typeof options.enabled !== 'boolean') throw new Error('Invalid counterverification policy');
|
|
14
|
+
if (options.isFatalError !== undefined && typeof options.isFatalError !== 'function') throw new Error('Invalid counterverification fatal predicate');
|
|
15
|
+
const results = structuredClone(baseline);
|
|
16
|
+
if (options.enabled !== true) return results;
|
|
17
|
+
const indices = candidates.flatMap((candidate, index) => ['critical', 'high'].includes(candidate?.severity) && confirmed(baseline[index]) ? [index] : []);
|
|
18
|
+
if (!indices.length) return results;
|
|
19
|
+
const independent = indices.map(index => {
|
|
20
|
+
const value = structuredClone(candidates[index]);
|
|
21
|
+
delete value.verification;
|
|
22
|
+
delete value.counterVerification;
|
|
23
|
+
return value;
|
|
24
|
+
});
|
|
25
|
+
let counter, failureCode;
|
|
26
|
+
try {
|
|
27
|
+
counter = await verificationLoop(model, request, independent, scope, { ...options, verificationFocus: 'counterevidence', correctProtocol: false });
|
|
28
|
+
request.signal?.throwIfAborted();
|
|
29
|
+
} catch (error) {
|
|
30
|
+
request.signal?.throwIfAborted();
|
|
31
|
+
if (error?.name === 'AbortError' || fatal(error, options)) throw error;
|
|
32
|
+
// Never echo an untrusted model exception (which may contain source text).
|
|
33
|
+
failureCode = 'COUNTER_VERIFICATION_INCOMPLETE';
|
|
34
|
+
}
|
|
35
|
+
indices.forEach((index, offset) => {
|
|
36
|
+
const original = baseline[index], extra = counter?.[offset];
|
|
37
|
+
const metadata = extra ? { status: 'completed', result: structuredClone(extra), baselineResult: structuredClone(original) } : { status: 'incomplete', failureCode, baselineResult: structuredClone(original) };
|
|
38
|
+
results[index].counterVerification = metadata;
|
|
39
|
+
if (extra && confirmed(extra) && comparable(original) && comparable(extra) && classification(original) === classification(extra)) return;
|
|
40
|
+
// Preserve both independent records. Uncertainty is a conservative aggregate,
|
|
41
|
+
// not a new model verdict or evidence borrowed from the second receipt table.
|
|
42
|
+
results[index].verdict = 'uncertain';
|
|
43
|
+
results[index].reason = extra ? 'Independent counterevidence verification did not confirm the same supported classification.' : 'Independent counterevidence verification is incomplete.';
|
|
44
|
+
results[index].causality = 'unverified';
|
|
45
|
+
results[index].regression = { classification: 'uncertain', reason: results[index].reason, oldEvidence: [], newEvidence: [], causality: 'unverified' };
|
|
46
|
+
});
|
|
47
|
+
return results;
|
|
48
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
// Navigation and human disposition metadata only. Never a source/evidence receipt,
|
|
4
|
+
// verification result, coverage certificate, approval, or permission to run again.
|
|
5
|
+
const MAX_CYCLES = 3;
|
|
6
|
+
const LIMIT = 10000;
|
|
7
|
+
const TRUST = 'historical-metadata-not-evidence';
|
|
8
|
+
const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
9
|
+
const fail = message => { throw new Error('Finding ledger: ' + message); };
|
|
10
|
+
const text = value => typeof value === 'string' && value.trim().length > 0 && value.length <= 16000;
|
|
11
|
+
const hash = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
12
|
+
const record = value => value && typeof value === 'object' && !Array.isArray(value) && [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
13
|
+
function exact(value, keys) {
|
|
14
|
+
if (!record(value) || Reflect.ownKeys(value).some(key => typeof key !== 'string' || !keys.includes(key) || !Object.hasOwn(Object.getOwnPropertyDescriptor(value, key), 'value'))) fail('unknown or invalid fields');
|
|
15
|
+
}
|
|
16
|
+
function list(value) { if (!Array.isArray(value) || value.length > LIMIT) fail('invalid or excessive list'); }
|
|
17
|
+
function path(value) {
|
|
18
|
+
// SVN uses the canonical repository root for directory property changes.
|
|
19
|
+
if (value === '.') return value;
|
|
20
|
+
if (!text(value) || value.includes('\\') || value.includes('\0') || value.startsWith('/') || /^[a-z]:/i.test(value) || value.split('/').some(part => !part || part === '.' || part === '..')) fail('invalid repository-relative path');
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function identityValue(value) {
|
|
24
|
+
const keys = ['repository', 'route', 'rulesHash', 'requirementPolicyHash'];
|
|
25
|
+
exact(value, keys);
|
|
26
|
+
if (!text(value.repository) || !text(value.route) || !hash(value.rulesHash) || !hash(value.requirementPolicyHash)) fail('invalid identity');
|
|
27
|
+
return Object.fromEntries(keys.map(key => [key, value[key]]));
|
|
28
|
+
}
|
|
29
|
+
function sameIdentity(left, right) { if (JSON.stringify(left) !== JSON.stringify(right)) fail('identity mismatch'); }
|
|
30
|
+
function freeze(value) {
|
|
31
|
+
if (value && typeof value === 'object') { for (const item of Object.values(value)) freeze(item); Object.freeze(value); }
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
function decisionValue(value) {
|
|
35
|
+
exact(value, ['decision', 'reason', 'cycle']);
|
|
36
|
+
if (!['fixed', 'refuted', 'intentional', 'deferred'].includes(value.decision) || !text(value.reason) || !Number.isInteger(value.cycle) || value.cycle < 1 || value.cycle > MAX_CYCLES) fail('invalid human decision');
|
|
37
|
+
return { decision: value.decision, reason: value.reason, cycle: value.cycle };
|
|
38
|
+
}
|
|
39
|
+
function validatePrevious(value, identity) {
|
|
40
|
+
exact(value, ['schemaVersion', 'identity', 'originSnapshotId', 'chainOrigin', 'cycle', 'maxCycles', 'nextAction', 'trust', 'snapshotId', 'findings']);
|
|
41
|
+
sameIdentity(identityValue(value.identity), identity);
|
|
42
|
+
if (value.schemaVersion !== 1 || value.trust !== TRUST || value.maxCycles !== MAX_CYCLES || !Number.isInteger(value.cycle) || value.cycle < 1 || value.cycle > MAX_CYCLES || !text(value.snapshotId) || !text(value.originSnapshotId) || value.chainOrigin !== digest([identity, value.originSnapshotId]) || value.nextAction !== (value.cycle === MAX_CYCLES ? 'step-back' : 'human-review')) fail('invalid chain');
|
|
43
|
+
list(value.findings);
|
|
44
|
+
const ids = new Set();
|
|
45
|
+
const findings = value.findings.map(item => {
|
|
46
|
+
exact(item, ['id', 'path', 'side', 'anchorHash', 'claimHash', 'matchable', 'firstSeen', 'lastSeen', 'observation', 'findingIndices', 'humanDecisions']);
|
|
47
|
+
path(item.path);
|
|
48
|
+
if (item.path === '.' && !['property', 'file'].includes(item.side)) fail('root findings require property or file anchor');
|
|
49
|
+
if (!hash(item.id) || ids.has(item.id) || !hash(item.anchorHash) || !hash(item.claimHash) || !['old', 'new', 'property', 'file'].includes(item.side) || typeof item.matchable !== 'boolean' || item.matchable !== (item.side !== 'file') || !['new', 'repeated', 'not-observed'].includes(item.observation) || !Number.isInteger(item.firstSeen) || !Number.isInteger(item.lastSeen) || item.firstSeen < 1 || item.lastSeen < item.firstSeen || item.lastSeen > value.cycle) fail('invalid historical finding');
|
|
50
|
+
list(item.findingIndices); list(item.humanDecisions);
|
|
51
|
+
if (item.findingIndices.some(index => !Number.isSafeInteger(index) || index < 0) || new Set(item.findingIndices).size !== item.findingIndices.length) fail('invalid finding indices');
|
|
52
|
+
const humanDecisions = item.humanDecisions.map(decisionValue);
|
|
53
|
+
if (humanDecisions.length > MAX_CYCLES || humanDecisions.some(d => d.cycle > value.cycle) || new Set(humanDecisions.map(d => d.cycle)).size !== humanDecisions.length) fail('invalid decision history');
|
|
54
|
+
const expected = digest([identity.repository, item.path, item.side, item.anchorHash, item.claimHash, item.matchable ? null : value.chainOrigin, item.matchable ? null : item.firstSeen]);
|
|
55
|
+
if (item.id !== expected) fail('invalid finding fingerprint');
|
|
56
|
+
ids.add(item.id);
|
|
57
|
+
return { ...item, findingIndices: [], humanDecisions };
|
|
58
|
+
});
|
|
59
|
+
return { ...value, findings };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Accepts the real report.findings shape from review.mjs (including project reports).
|
|
63
|
+
* Does not mutate or copy report verification, evidence receipts, status or source.
|
|
64
|
+
* Hashes exact source anchor and defect claim; prose drift deliberately makes a new
|
|
65
|
+
* candidate rather than heuristically merging distinct causes by title/path.
|
|
66
|
+
*/
|
|
67
|
+
export function buildFindingLedger(report, identity, options = {}) {
|
|
68
|
+
identity = identityValue(identity);
|
|
69
|
+
exact(options, ['previous', 'outcomes']);
|
|
70
|
+
if (!record(report) || !text(report.snapshotId)) fail('invalid current report');
|
|
71
|
+
list(report.findings);
|
|
72
|
+
const previous = options.previous === undefined ? undefined : validatePrevious(options.previous, identity);
|
|
73
|
+
if (previous?.cycle === MAX_CYCLES) fail('cycle cap reached; step back with human review, not automatic approval');
|
|
74
|
+
const cycle = (previous?.cycle ?? 0) + 1;
|
|
75
|
+
const originSnapshotId = previous?.originSnapshotId ?? report.snapshotId;
|
|
76
|
+
const chainOrigin = digest([identity, originSnapshotId]);
|
|
77
|
+
const rows = new Map((previous?.findings ?? []).map(item => [item.id, { ...item, observation: 'not-observed' }]));
|
|
78
|
+
report.findings.forEach((finding, index) => {
|
|
79
|
+
if (!record(finding)) fail('invalid current finding');
|
|
80
|
+
const mapped = finding.path === undefined && Array.isArray(report.files) ? report.files.filter(file => file.fileId === finding.fileId && (finding.batchSnapshotId === undefined || file.batchSnapshotId === finding.batchSnapshotId)) : [];
|
|
81
|
+
const findingPath = path(finding.path ?? (mapped.length === 1 ? mapped[0].path : undefined)), anchor = finding.anchor;
|
|
82
|
+
if (!record(anchor) || !['line', 'property', 'file'].includes(anchor.kind)) fail('invalid current anchor');
|
|
83
|
+
if (findingPath === '.' && !['property', 'file'].includes(anchor.kind)) fail('root findings require property or file anchor');
|
|
84
|
+
const side = anchor.kind === 'line' ? anchor.side : anchor.kind;
|
|
85
|
+
if (anchor.kind === 'line' && (!['old', 'new'].includes(side) || !text(anchor.snippet))) fail('missing exact snippet/side');
|
|
86
|
+
if (anchor.kind === 'property' && !text(anchor.name)) fail('missing exact property');
|
|
87
|
+
for (const key of ['evidence', 'trigger', 'impact']) if (!text(finding[key])) fail('missing defect claim');
|
|
88
|
+
const a = finding.attribution;
|
|
89
|
+
const causal = ['beforeBehavior', 'afterBehavior', 'reason'].map(key => {
|
|
90
|
+
if (a?.[key] !== undefined && !text(a[key])) fail('invalid causal claim');
|
|
91
|
+
return a?.[key] ?? null;
|
|
92
|
+
});
|
|
93
|
+
// Qualified interaction refs differentiate cross-file claims without transferring receipts.
|
|
94
|
+
const interaction = finding.interaction?.editRefs;
|
|
95
|
+
if (interaction !== undefined) {
|
|
96
|
+
list(interaction);
|
|
97
|
+
for (const ref of interaction) if (!record(ref) || !text(ref.fileId) || !text(ref.editId)) fail('invalid interaction claim');
|
|
98
|
+
}
|
|
99
|
+
const claimHash = digest([finding.evidence, finding.trigger, finding.impact, causal, interaction?.map(ref => [ref.fileId, ref.editId]).sort() ?? null]);
|
|
100
|
+
const anchorHash = digest(anchor.kind === 'line' ? anchor.snippet : anchor.kind === 'property' ? anchor.name : 'file');
|
|
101
|
+
const matchable = anchor.kind !== 'file';
|
|
102
|
+
const id = digest([identity.repository, findingPath, side, anchorHash, claimHash, matchable ? null : chainOrigin, matchable ? null : cycle]);
|
|
103
|
+
const existing = rows.get(id);
|
|
104
|
+
if (existing) { existing.observation = existing.firstSeen === cycle ? 'new' : 'repeated'; existing.lastSeen = cycle; existing.findingIndices.push(index); }
|
|
105
|
+
else rows.set(id, { id, path: findingPath, side, anchorHash, claimHash, matchable, firstSeen: cycle, lastSeen: cycle, observation: 'new', findingIndices: [index], humanDecisions: [] });
|
|
106
|
+
});
|
|
107
|
+
const outcomes = options.outcomes ?? [];
|
|
108
|
+
list(outcomes);
|
|
109
|
+
const decided = new Set();
|
|
110
|
+
for (const outcome of outcomes) {
|
|
111
|
+
exact(outcome, ['id', 'decision', 'reason']);
|
|
112
|
+
const item = rows.get(outcome.id);
|
|
113
|
+
if (!item || decided.has(outcome.id)) fail('unknown or duplicate outcome id');
|
|
114
|
+
decided.add(outcome.id);
|
|
115
|
+
item.humanDecisions.push(decisionValue({ decision: outcome.decision, reason: outcome.reason, cycle }));
|
|
116
|
+
}
|
|
117
|
+
if (rows.size > LIMIT) fail('excessive historical findings');
|
|
118
|
+
return freeze({ schemaVersion: 1, identity, originSnapshotId, chainOrigin, cycle, maxCycles: MAX_CYCLES, nextAction: cycle === MAX_CYCLES ? 'step-back' : 'human-review', trust: TRUST, snapshotId: report.snapshotId, findings: [...rows.values()] });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function snapshotMetadata(value) {
|
|
122
|
+
exact(value, ['identity', 'snapshotId', 'files']);
|
|
123
|
+
const identity = identityValue(value.identity);
|
|
124
|
+
if (!text(value.snapshotId)) fail('invalid snapshot metadata');
|
|
125
|
+
list(value.files);
|
|
126
|
+
const files = new Map();
|
|
127
|
+
for (const file of value.files) {
|
|
128
|
+
exact(file, ['path', 'hash', 'dependencies']);
|
|
129
|
+
const name = path(file.path);
|
|
130
|
+
if (files.has(name) || file.hash !== undefined && !hash(file.hash)) fail('invalid or duplicate source hash');
|
|
131
|
+
const dependencies = file.dependencies;
|
|
132
|
+
if (dependencies !== undefined) {
|
|
133
|
+
list(dependencies);
|
|
134
|
+
const seen = new Set();
|
|
135
|
+
for (const dep of dependencies) {
|
|
136
|
+
exact(dep, ['path', 'hash']); path(dep.path);
|
|
137
|
+
if (seen.has(dep.path) || dep.hash !== undefined && !hash(dep.hash)) fail('invalid dependency');
|
|
138
|
+
seen.add(dep.path);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
files.set(name, file);
|
|
142
|
+
}
|
|
143
|
+
return { identity, files };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Explicit complete dependency lists are caller-supplied metadata, not discovery
|
|
147
|
+
* or proof. Unknown/changing deps conservatively invalidate their transitive users.
|
|
148
|
+
* Even unchanged paths are navigation only, never full coverage or reused evidence.
|
|
149
|
+
*/
|
|
150
|
+
export function planIncrementalReview(previous, current) {
|
|
151
|
+
const old = snapshotMetadata(previous), next = snapshotMetadata(current);
|
|
152
|
+
sameIdentity(old.identity, next.identity);
|
|
153
|
+
const safe = new Set();
|
|
154
|
+
for (const [name, file] of next.files) {
|
|
155
|
+
const before = old.files.get(name);
|
|
156
|
+
if (!before || !hash(file.hash) || file.hash !== before.hash || !Array.isArray(file.dependencies) || !Array.isArray(before.dependencies)) continue;
|
|
157
|
+
const deps = list => list.map(dep => [dep.path, dep.hash]).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
158
|
+
if (JSON.stringify(deps(file.dependencies)) !== JSON.stringify(deps(before.dependencies))) continue;
|
|
159
|
+
if (file.dependencies.every(dep => hash(dep.hash) && next.files.get(dep.path)?.hash === dep.hash && old.files.get(dep.path)?.hash === dep.hash)) safe.add(name);
|
|
160
|
+
}
|
|
161
|
+
// Fixed point is cycle-safe: an unchanged, fully-known dependency cycle is safe
|
|
162
|
+
// navigation; any unknown/changed member invalidates every reachable dependent.
|
|
163
|
+
let changed = true;
|
|
164
|
+
while (changed) {
|
|
165
|
+
changed = false;
|
|
166
|
+
for (const name of safe) if (next.files.get(name).dependencies.some(dep => !safe.has(dep.path))) { safe.delete(name); changed = true; }
|
|
167
|
+
}
|
|
168
|
+
return freeze({ freshReviewPaths: [...next.files.keys()].filter(name => !safe.has(name)), unchangedNavigationPaths: [...next.files.keys()].filter(name => safe.has(name)), removedPaths: [...old.files.keys()].filter(name => !next.files.has(name)), coverage: false, trust: TRUST, requiresFreshEvidence: true });
|
|
169
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { modelResultTools } from './model-tools.mjs';
|
|
3
|
+
import { childOutputSchema } from './child-output-schema.mjs';
|
|
4
|
+
import { parseRetrievalResponse, executeRetrievalBatch } from './retrieval-protocol.mjs';
|
|
5
|
+
import { registerReviewChild } from './review-child-broker.mjs';
|
|
6
|
+
|
|
7
|
+
export const REVIEW_CHILD_PRESET = 'dsh-code-review-restricted';
|
|
8
|
+
const failure = (code, message) => Object.assign(new Error(message), { code });
|
|
9
|
+
// One managed spawn activation owns the entire primary loop, including snapshot tools.
|
|
10
|
+
// No credentials or free-form filesystem tools are passed into that activation.
|
|
11
|
+
export function createHostReviewChildren(ctx) {
|
|
12
|
+
let ready, unregisterPreset;
|
|
13
|
+
const service = () => ctx.get?.('subagents');
|
|
14
|
+
const presets = () => ctx.get?.('agentPresets');
|
|
15
|
+
function capability() {
|
|
16
|
+
const runtime = service(), provider = runtime?.getProvider?.('spawn');
|
|
17
|
+
return !!(provider?.capabilities?.outputSchema && provider.capabilities.agentPreset && provider.capabilities.agentOptions && runtime.resolveMaxDepth && presets()?.register && ctx.get?.('agents')?.get);
|
|
18
|
+
}
|
|
19
|
+
async function prepare() {
|
|
20
|
+
if (!capability()) throw failure('REVIEW_SUBAGENTS_UNAVAILABLE', 'Restricted host spawn subagents are unavailable; no hidden direct fallback');
|
|
21
|
+
ready ??= presets().register({ id: REVIEW_CHILD_PRESET, name: 'Read-only snapshot review', plugins: [
|
|
22
|
+
{ name: '@deepseek-ai/dsh-tool-executor-native' },
|
|
23
|
+
{ name: '@feng0531/dsh-code-review/child' },
|
|
24
|
+
] }).then(dispose => { unregisterPreset = dispose; });
|
|
25
|
+
await ready;
|
|
26
|
+
}
|
|
27
|
+
return { capability, prepare,
|
|
28
|
+
async create({ parent, route, cwd, signal }) {
|
|
29
|
+
await prepare(); signal.throwIfAborted();
|
|
30
|
+
const runtime = service(), childId = randomUUID();
|
|
31
|
+
let handle, revoked, stop, started = false;
|
|
32
|
+
const child = { transport: 'host-subagent', childId,
|
|
33
|
+
async runPrimary({ request, scope, maxRounds, maxInputBytes, reserveModelCall, onRetrieved, onBudgetLimited, onTruncated, onProgress }) {
|
|
34
|
+
if (started) throw new Error('A review child cannot be reused for another primary');
|
|
35
|
+
started = true; let rounds = 0, modelCalls = 0, bindingFatal;
|
|
36
|
+
const abortBinding = error => { bindingFatal ??= error; handle?.dispose().catch(() => {}); };
|
|
37
|
+
const assertAgent = agent => {
|
|
38
|
+
if (!agent || agent.id !== childId || agent.session.header.parentSession !== (parent.session.header.id ?? parent.id) || agent.session.header.agentPreset !== REVIEW_CHILD_PRESET) throw failure('REVIEW_CHILD_IDENTITY', 'Review child ownership changed');
|
|
39
|
+
};
|
|
40
|
+
const binding = { parent, parentId: parent.session.header.id ?? parent.id, presetId: REVIEW_CHILD_PRESET, route, assertAgent, onFatal: abortBinding,
|
|
41
|
+
beforeModelCall(options) {
|
|
42
|
+
try {
|
|
43
|
+
signal.throwIfAborted();
|
|
44
|
+
if (bindingFatal) throw bindingFatal;
|
|
45
|
+
if (options.provider !== route.provider || options.model !== route.model || options.reasoningEffort !== route.reasoningEffort) throw failure('REVIEW_CHILD_ROUTE', 'Review child model route changed');
|
|
46
|
+
// This is the complete logical LLM envelope, not provider-specific HTTP bytes.
|
|
47
|
+
const bytes = Buffer.byteLength(JSON.stringify({ system: options.system, messages: options.messages, tools: options.tools }));
|
|
48
|
+
if (bytes > maxInputBytes) throw failure('WHOLE_SCOPE_INPUT_BUDGET', 'Child logical model input exceeds approved capacity');
|
|
49
|
+
if (modelCalls >= maxRounds + 2) throw failure('WHOLE_SCOPE_MODEL_CALL_LIMIT', 'Child primary loop call limit exceeded');
|
|
50
|
+
reserveModelCall(); modelCalls++;
|
|
51
|
+
} catch (error) { abortBinding(error); throw error; }
|
|
52
|
+
},
|
|
53
|
+
executeRequests(args, exec) {
|
|
54
|
+
assertAgent(exec.agent); signal.throwIfAborted();
|
|
55
|
+
if (bindingFatal) throw bindingFatal;
|
|
56
|
+
const requests = parseRetrievalResponse(args);
|
|
57
|
+
if (rounds >= maxRounds) {
|
|
58
|
+
const info = { code: 'RETRIEVAL_ROUND_BUDGET_EXCEEDED', reason: 'Child snapshot tool rounds exhausted' };
|
|
59
|
+
onBudgetLimited?.(info); throw failure(info.code, info.reason);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
scope.preflight(requests);
|
|
63
|
+
const results = scope.transaction(() => executeRetrievalBatch(args, scope, signal));
|
|
64
|
+
rounds++;
|
|
65
|
+
for (const { request: operation, result } of results) {
|
|
66
|
+
if (result.truncated) onTruncated?.();
|
|
67
|
+
onRetrieved?.([{ kind: operation.kind, snapshotId: result.snapshotId, stage: 'retrieved-locally', sourceId: result.sourceId, hash: result.hash, start: result.start, count: result.count, resultBytes: Buffer.byteLength(JSON.stringify(result)) }]);
|
|
68
|
+
}
|
|
69
|
+
return { snapshotId: JSON.parse(request.input).snapshotId, results, budget: scope.budget() };
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (['READER_CALL_BUDGET_EXCEEDED','READER_OUTPUT_BUDGET_EXCEEDED'].includes(error.code)) onBudgetLimited?.({code:error.code,reason:error.message});
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
revoked = registerReviewChild(childId, binding);
|
|
77
|
+
const finalEnvelope = JSON.parse(request.input); finalEnvelope.retrievalFinalization = { reason: 'Use review_snapshot tool for supplemental reading; structured_output submits final reviews only' };
|
|
78
|
+
const schema = childOutputSchema(modelResultTools(JSON.stringify(finalEnvelope), request.instructions)[0].parameters);
|
|
79
|
+
stop = () => { handle?.dispose().catch(() => {}); }; signal.addEventListener('abort', stop, { once: true });
|
|
80
|
+
try {
|
|
81
|
+
handle = await runtime.startActivation({ provider: 'spawn', childId, label: 'Whole-scope review batch', request: {
|
|
82
|
+
parent, cwd, agentPreset: REVIEW_CHILD_PRESET, maxDepth: runtime.resolveMaxDepth(undefined),
|
|
83
|
+
agentOptions: { ...route, maxTokens: 4096 }, outputSchema: schema,
|
|
84
|
+
prompt: [{ type: 'text', text: request.instructions + '\nUse only review_snapshot for approved catalog sources. Preserve this child context for all supplemental reads. Return final reviews via structured_output, never requests.\n' + JSON.stringify({ ...JSON.parse(request.input), catalog: scope.catalog(), retrievalBudget: scope.budget() }) }],
|
|
85
|
+
}, signal, delivery: 'caller' });
|
|
86
|
+
if (signal.aborted || bindingFatal) await handle.dispose();
|
|
87
|
+
const result = await handle.result;
|
|
88
|
+
signal.throwIfAborted(); if (bindingFatal) throw bindingFatal;
|
|
89
|
+
if (result.stopReason !== 'completed' || !result.structured) throw failure('REVIEW_CHILD_INCOMPLETE', 'Host child did not commit a structured final result');
|
|
90
|
+
await onProgress?.({stage:'host-child-complete',childId,modelCalls});
|
|
91
|
+
return result.structured;
|
|
92
|
+
} catch (error) { throw bindingFatal ?? error; } finally { signal.removeEventListener('abort', stop); }
|
|
93
|
+
},
|
|
94
|
+
async dispose() { revoked?.(); if (handle) await handle.dispose(); },
|
|
95
|
+
};
|
|
96
|
+
return child;
|
|
97
|
+
},
|
|
98
|
+
async dispose() { if (ready) await ready; await unregisterPreset?.(); },
|
|
99
|
+
};
|
|
100
|
+
}
|