@feng0531/dsh-code-review 0.20.1 → 0.21.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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # 变更记录
2
2
 
3
+ ## 0.21.0
4
+
5
+ - 注册入口开启唯一精确锚纠正,跨文件限定原组候选主ID,原候选/状态JSON保留及Markdown提示。
6
+ - 仍校验原qualified编辑/读取/复核;故意错行C++综合全链6调用通过,全量250及宿主离线通过,未宣称整体源码对齐。
7
+
8
+ ## 0.20.2
9
+
10
+ - 可选主文件唯一全行原文锚纠正,仅同侧且重叠原精确编辑,保留原完整候选/状态,仍经原验证,不加调用权限。
11
+ - 歧义/无引用/不重叠拒绝,有界CPU及取消;注册/跨文件纠正未开启,全量250通过。
12
+
3
13
  ## 0.20.1
4
14
 
5
15
  - SVN自动上下文使用离线正常节点索引,排除外部/切换/复制/冲突/属性变化,双读及规范化完整状态指纹一致性验证。
package/README.md CHANGED
@@ -4,7 +4,9 @@
4
4
 
5
5
  ## 当前状态
6
6
 
7
- 当前代码版本为 0.20.1
7
+ 当前代码版本为 0.21.0
8
+
9
+ 工具对错误行锚可进行一次同文件同侧唯一完整原文纠正,须重叠原引用精确编辑;原候选/状态保留JSON,Markdown提示。纠正后仍校验窗口、跨文件编辑及生成读取证据,不增调用、不猜歧义或跨文件迁移,不证明缺陷或因果。
8
10
 
9
11
  可在预览传入 `autoContext: true`,显式选择有界Git/SVN直接import/include上下文捕获;默认关闭。候选、阻塞、截断及实际路径在预览可见,正文仅在确认后发送,执行重新验证快照。仅当前侧导航非历史/调用图,SVN使用离线正常节点索引支持同样导航;不会自动引入未跟踪或排除变更。,包含有界覆盖预检、显式项目规则及候选证据与反证复核;发布状态以 npm 和 GitHub Release 为准。已通过真实临时 Git/SVN 仓库测试,以及隔离进程中的真实 DSH ToolRuntime 注册验证。当前开发会话的实际预览已返回0.6.0的plan/rules字段,空变更预览与默认关闭外发已验证;已进行经宿主审批的六次小规模真实试跑,累计12次模型调用;四组合成正负例与历史修复对照符合预期,历史引入方向检出候选但引用复核未完成(partial)。不是独立盲测或整体质量证明。详情见 [试跑结果](evaluation/PILOT-RESULTS.md)。
10
12
 
package/index.mjs CHANGED
@@ -5,7 +5,7 @@ export const inject = ['tools', 'llm'];
5
5
  export function apply(ctx, config = {}) {
6
6
  // This entry runs only when the host invokes the registered tool.
7
7
  // Host tool policy owns permission; do not request a second approval here.
8
- const service = createReviewService(ctx.llm, { allowModelSending: true, enableRiskPlanning: true, enableBusinessGrouping: true, authorize: async () => true });
8
+ const service = createReviewService(ctx.llm, { allowModelSending: true, enableRiskPlanning: true, enableBusinessGrouping: true, enableAnchorCorrection: true, authorize: async () => true });
9
9
  const output = {
10
10
  schema: { type: 'object', additionalProperties: false, properties: { json: { type: 'string' }, markdown: { type: 'string' } }, required: ['json', 'markdown'] },
11
11
  render: (_args, value) => [{ type: 'text', text: value.markdown || value.json }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feng0531/dsh-code-review",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "private": false,
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
@@ -0,0 +1,18 @@
1
+ // Pure, single-attempt exact correction. Never searches another file or source side.
2
+ export function correctAnchor(raw, file, changes, {signal}={}) {
3
+ signal?.throwIfAborted();const a=raw?.anchor;
4
+ const result=(status,reason)=>({status,reason,originalCandidate:structuredClone(raw)});
5
+ if(!raw||raw.fileId!==file.id||a?.kind!=='line'||!['old','new'].includes(a.side)||!Number.isSafeInteger(a.start)||!Number.isSafeInteger(a.end)||a.start<1||a.end<a.start||typeof a.snippet!=='string'||!a.snippet.trim()||a.snippet.length>8000)return result('not-applicable','invalid-anchor');
6
+ const text=(a.side==='old'?file.left:file.right)?.text;if(typeof text!=='string'||Buffer.byteLength(text)>256*1024)return result('not-applicable','source-limit');
7
+ const source=text.replaceAll('\r\n','\n').split('\n'),snippet=a.snippet.replaceAll('\r\n','\n').split('\n');
8
+ if(source.slice(a.start-1,a.end).join('\n')===snippet.join('\n'))return result('unchanged','already-exact');
9
+ if(changes?.status!=='changed'||changes.precision!=='exact'||!Array.isArray(raw.attribution?.editIds)||!raw.attribution.editIds.length)return result('rejected','missing-exact-edit');
10
+ const edits=changes.edits.filter(e=>raw.attribution.editIds.includes(e.id));if(edits.length!==raw.attribution.editIds.length)return result('rejected','unknown-or-duplicate-edit');
11
+ // Bound CPU even for a long multiline snippet; no silent partial search acceptance.
12
+ if(source.length*snippet.length>2000000)return result('rejected','search-work-limit');
13
+ let match;for(let i=0;i+snippet.length<=source.length;i++){signal?.throwIfAborted();if(!snippet.every((line,j)=>source[i+j]===line))continue;if(match!==undefined)return result('rejected','ambiguous-exact-text');match=i;}
14
+ if(match===undefined)return result('rejected','no-exact-text');
15
+ const start=match+1,end=match+snippet.length;
16
+ if(!edits.some(e=>e[a.side].count>0&&start<e[a.side].start+e[a.side].count&&end>=e[a.side].start))return result('rejected','does-not-overlap-referenced-edit');
17
+ const candidate=structuredClone(raw);candidate.anchor={...a,start,end};return {...result('corrected','unique-exact-text-overlaps-edit'),candidate};
18
+ }
package/src/review.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { correctAnchor } from './anchor-correction.mjs';
1
2
  import { BUSINESS_GROUP_INSTRUCTIONS, prepareBusinessGroups, validateBusinessGroups } from './business-groups.mjs';
2
3
  import { RISK_PLAN_INSTRUCTIONS, validateRiskPlan } from './risk-plan.mjs';
3
4
  import { prepareInteractionInput } from './interaction-input.mjs';
@@ -167,7 +168,21 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
167
168
  }
168
169
  const request = { ...enclosingRequest, input: batchPayload };
169
170
  const auditStart = state.retrievalAudit?.length ?? 0;
170
- const response = await executor(request);
171
+ let response = await executor(request);
172
+ if (options.enableAnchorCorrection === true) {
173
+ if (Buffer.byteLength(typeof response === 'string' ? response : JSON.stringify(response)) > 128 * 1024) throw new Error('Review response too large');
174
+ const decoded = typeof response === 'string' ? JSON.parse(response) : structuredClone(response);
175
+ if (Array.isArray(decoded?.findings) && decoded.findings.length <= 50) {
176
+ decoded.findings = decoded.findings.map(raw => {
177
+ const target = batch.interaction ? input.files.find(f => f.id === raw?.fileId && batch.interaction.fileIds.includes(f.id) && f.eligibility === 'reviewable') : file;
178
+ if (!target) return raw;
179
+ const correction = correctAnchor(raw, target, target === file ? changes : changeMap(target.left.text,target.right.text), { signal: request.signal });
180
+ if (!['unchanged','not-applicable'].includes(correction.status)) (state.anchorCorrections ??= []).push(correction);
181
+ return correction.status === 'corrected' ? correction.candidate : raw;
182
+ });
183
+ response = decoded;
184
+ }
185
+ }
171
186
  const generated = batch.interaction ? validateInteractionFindings(response, input, batch.interaction, (state.retrievalAudit ?? []).slice(auditStart), scope.catalog()) : validateResponse(response, file, changes);
172
187
  if (batch.synthesis) validateSynthesisReads(generated.findings, changes, (state.retrievalAudit ?? []).slice(auditStart), scope.catalog(), file.id, input.id);
173
188
  if (!batch.interaction && state.sourceMode === 'change-windows') validateWindowFindings(generated.findings, batch.synthesis ? prepared.synthesis.windows : JSON.parse(batch.payload).windows, batch.synthesis === true);
@@ -235,6 +250,7 @@ const safe = text => Array.from(String(text ?? '')).map(c => {
235
250
  }).join('');
236
251
  export function markdownReport(report) {
237
252
  const output = ['# Code Review 报告', '', '状态:' + report.status, '快照:' + report.snapshotId, '', '## 覆盖情况'];
253
+ for (const file of report.files) for (const correction of file.anchorCorrections ?? []) output.push('- 定位纠正:' + safe(correction.originalCandidate.fileId) + ' · ' + safe(correction.status) + ' · ' + safe(correction.reason) + ';原完整候选保留JSON,仅定位不证明因果');
238
254
  if (report.businessGrouping) output.push('- 业务分组:' + safe(report.businessGrouping.status) + (report.businessGrouping.reason ? ' — ' + safe(report.businessGrouping.reason) : '') + ';仅调度假设,不证明依赖或覆盖');
239
255
  for (const file of report.files) for (const plan of file.riskPlans ?? []) output.push('- 风险计划:' + safe(file.path) + ' · ' + safe(plan.status) + ' · ' + plan.risks.length + '项待证假设;不表示缺陷成立或覆盖完成');
240
256
  for (const file of report.files) output.push('- ' + safe(file.path) + ':' + file.status + (file.reason ? ' — ' + safe(file.reason) : ''));
package/src/service.mjs CHANGED
@@ -14,7 +14,7 @@ function owner(exec) {
14
14
  if (!agent || typeof cwd !== 'string' || !cwd || !agent.options?.provider || !agent.options?.model) throw new Error('Active Agent with cwd and model is required');
15
15
  return { agent, cwd, route: { provider: agent.options.provider, model: agent.options.model, ...(agent.options.reasoningEffort ? { reasoningEffort: agent.options.reasoningEffort } : {}) } };
16
16
  }
17
- export function createReviewService(llm, { capture = captureSnapshot, now = Date.now, ttlMs = 300000, maxPreviews = 8, allowModelSending = false, enableRiskPlanning = false, enableBusinessGrouping = false, authorize = async () => false } = {}) {
17
+ export function createReviewService(llm, { capture = captureSnapshot, now = Date.now, ttlMs = 300000, maxPreviews = 8, allowModelSending = false, enableRiskPlanning = false, enableBusinessGrouping = false, enableAnchorCorrection = false, authorize = async () => false } = {}) {
18
18
  for (const value of [ttlMs, maxPreviews]) if (!Number.isSafeInteger(value) || value < 1) throw new Error('Invalid preview limit');
19
19
  const lifetime = new AbortController();
20
20
  const bind = exec => ({ ...exec, signal: exec.signal ? AbortSignal.any([exec.signal, lifetime.signal]) : lifetime.signal });
@@ -65,7 +65,7 @@ export function createReviewService(llm, { capture = captureSnapshot, now = Date
65
65
  if (await authorize({ snapshot, route: preview.route, exec }) !== true) throw new Error('Model sending approval denied or unavailable');
66
66
  exec.signal?.throwIfAborted();
67
67
  if (owner(exec).cwd !== current.cwd || JSON.stringify(owner(exec).route) !== JSON.stringify(preview.route)) throw new Error('Agent changed during approval');
68
- const report = await reviewSnapshot(snapshot, createDshModel(llm, preview.route), { signal: exec.signal, enableRetrieval: true, enableGrouping: true, enableVerification: true, enableRiskPlanning, enableBusinessGrouping });
68
+ const report = await reviewSnapshot(snapshot, createDshModel(llm, preview.route), { signal: exec.signal, enableRetrieval: true, enableGrouping: true, enableVerification: true, enableRiskPlanning, enableBusinessGrouping, enableAnchorCorrection });
69
69
  report.model = preview.route;
70
70
  const latest = await resolveWorkspaceRepository(current.cwd, preview.options.repositoryPath).then(target => target === preview.target ? capture(target, { ...preview.options, signal: exec.signal }) : null).catch(() => null);
71
71
  report.outdated = !latest || latest.id !== snapshot.id || owner(exec).cwd !== current.cwd;