@feng0531/dsh-code-review 0.20.0 → 0.20.2
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 +10 -0
- package/README.md +2 -2
- package/index.mjs +1 -1
- package/package.json +1 -1
- package/src/anchor-correction.mjs +18 -0
- package/src/review.mjs +18 -1
- package/src/snapshot.mjs +3 -4
- package/src/tracked-context.mjs +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.20.2
|
|
4
|
+
|
|
5
|
+
- 可选主文件唯一全行原文锚纠正,仅同侧且重叠原精确编辑,保留原完整候选/状态,仍经原验证,不加调用权限。
|
|
6
|
+
- 歧义/无引用/不重叠拒绝,有界CPU及取消;注册/跨文件纠正未开启,全量250通过。
|
|
7
|
+
|
|
8
|
+
## 0.20.1
|
|
9
|
+
|
|
10
|
+
- SVN自动上下文使用离线正常节点索引,排除外部/切换/复制/冲突/属性变化,双读及规范化完整状态指纹一致性验证。
|
|
11
|
+
- 报告保留自动上下文元数据及阻塞/截断限制;真实SVN及全量246通过,调用者/定义搜索待继续。
|
|
12
|
+
|
|
3
13
|
## 0.20.0
|
|
4
14
|
|
|
5
15
|
- 工具显式autoContext选择有界Git上下文,预览显示候选状态与实际路径不含正文,执行原选项复捕获哈希校验;默认关闭,SVN明确拒绝。
|
package/README.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
## 当前状态
|
|
6
6
|
|
|
7
|
-
当前代码版本为 0.20.
|
|
7
|
+
当前代码版本为 0.20.2
|
|
8
8
|
|
|
9
|
-
可在预览传入 `autoContext: true`,显式选择有界Git直接import/include上下文捕获;默认关闭。候选、阻塞、截断及实际路径在预览可见,正文仅在确认后发送,执行重新验证快照。仅当前侧导航非历史/调用图,SVN
|
|
9
|
+
可在预览传入 `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
10
|
|
|
11
11
|
## 工作区下多个子仓库(0.11.0)
|
|
12
12
|
|
package/index.mjs
CHANGED
|
@@ -12,7 +12,7 @@ export function apply(ctx, config = {}) {
|
|
|
12
12
|
};
|
|
13
13
|
ctx.tools.register({
|
|
14
14
|
name: 'code_review_preview', description: 'Preview Git/SVN working-copy 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. Preview each repository separately. Show the preview before execution.',
|
|
15
|
-
parameters: { type: 'object', additionalProperties: false, properties: { 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 bounded direct tracked Git import/include context captured and shown in preview
|
|
15
|
+
parameters: { type: 'object', additionalProperties: false, properties: { 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 bounded direct tracked Git/SVN import/include context captured and shown in preview.' }, contextPaths: { type: 'array', items: { type: 'string' }, maxItems: 20 }, rulePaths: { type: 'array', items: { type: 'string' }, maxItems: 4 } } },
|
|
16
16
|
output,
|
|
17
17
|
async execute(args, exec) { const preview = await service.preview(args, exec); return { json: JSON.stringify(preview), markdown: '' }; },
|
|
18
18
|
});
|
package/package.json
CHANGED
|
@@ -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';
|
|
@@ -88,6 +89,10 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
88
89
|
if (options.enableVerification === true && options.enableRetrieval !== true) throw new Error('Verification requires approved retrieval scope');
|
|
89
90
|
const scope = options.enableRetrieval === true ? createRetrievalScope({ ...input, context: input.context ?? [] }, { signal }) : null;
|
|
90
91
|
const report = { schemaVersion: 1, snapshotId: input.id, vcs: input.vcs, status: 'completed', files: [], findings: [], limitations: [], modelCalls: 0 };
|
|
92
|
+
if (input.autoContext) {
|
|
93
|
+
report.autoContext = structuredClone(input.autoContext);
|
|
94
|
+
if (input.autoContext.truncated || input.autoContext.candidates.some(c => c.status === 'blocked')) report.limitations.push({ text: '自动上下文未完全捕获:存在截断或阻塞,仅实际捕获路径可用于证据。' });
|
|
95
|
+
}
|
|
91
96
|
report.rules = (input.rules ?? []).map(({ path, hash }) => ({ path, hash }));
|
|
92
97
|
if (options.enableGrouping === true) report.grouping = buildReviewGroups(input.files, inferFileRelations(input.files));
|
|
93
98
|
report.interactions = [];
|
|
@@ -163,7 +168,19 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
163
168
|
}
|
|
164
169
|
const request = { ...enclosingRequest, input: batchPayload };
|
|
165
170
|
const auditStart = state.retrievalAudit?.length ?? 0;
|
|
166
|
-
|
|
171
|
+
let response = await executor(request);
|
|
172
|
+
if (options.enableAnchorCorrection === true && !batch.interaction) {
|
|
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 correction = correctAnchor(raw, file, changes, { signal: request.signal });
|
|
178
|
+
if (!['unchanged','not-applicable'].includes(correction.status)) (state.anchorCorrections ??= []).push(correction);
|
|
179
|
+
return correction.status === 'corrected' ? correction.candidate : raw;
|
|
180
|
+
});
|
|
181
|
+
response = decoded;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
167
184
|
const generated = batch.interaction ? validateInteractionFindings(response, input, batch.interaction, (state.retrievalAudit ?? []).slice(auditStart), scope.catalog()) : validateResponse(response, file, changes);
|
|
168
185
|
if (batch.synthesis) validateSynthesisReads(generated.findings, changes, (state.retrievalAudit ?? []).slice(auditStart), scope.catalog(), file.id, input.id);
|
|
169
186
|
if (!batch.interaction && state.sourceMode === 'change-windows') validateWindowFindings(generated.findings, batch.synthesis ? prepared.synthesis.windows : JSON.parse(batch.payload).windows, batch.synthesis === true);
|
package/src/snapshot.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { gitContextIndex } from './tracked-context.mjs';
|
|
1
|
+
import { gitContextIndex, svnContextIndex } from './tracked-context.mjs';
|
|
2
2
|
import { contextCandidates } from './context-candidates.mjs';
|
|
3
3
|
import { lstat, realpath } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -36,8 +36,7 @@ export async function captureSnapshot(cwd, options = {}) {
|
|
|
36
36
|
const context = [];
|
|
37
37
|
let autoContext, index;
|
|
38
38
|
if (options.autoContext === true) {
|
|
39
|
-
|
|
40
|
-
index = await gitContextIndex(snapshot.root, options);
|
|
39
|
+
index = await (type === 'git' ? gitContextIndex : svnContextIndex)(snapshot.root, options);
|
|
41
40
|
const plan = contextCandidates(snapshot.files, index.paths);
|
|
42
41
|
autoContext = { ...plan, candidates: plan.candidates.filter(c => !contextPaths.includes(c.path) && !rulePaths.includes(c.path)), capturedPaths: [] };
|
|
43
42
|
for (const candidate of autoContext.candidates) {
|
|
@@ -59,7 +58,7 @@ export async function captureSnapshot(cwd, options = {}) {
|
|
|
59
58
|
// Re-capture verifies both selected sides while collecting explicit context.
|
|
60
59
|
if ((await capture(cwd, options)).id !== snapshot.id) throw new Error('Snapshot changed while collecting context');
|
|
61
60
|
for (const item of context) if ((await readLocal(snapshot.root, item.path, options)).hash !== item.hash) throw new Error('Context changed during capture');
|
|
62
|
-
if (index && (await gitContextIndex(snapshot.root, options)).fingerprint !== index.fingerprint) throw new Error('Tracked context index changed during capture');
|
|
61
|
+
if (index && (await (type === 'git' ? gitContextIndex : svnContextIndex)(snapshot.root, options)).fingerprint !== index.fingerprint) throw new Error('Tracked context index changed during capture');
|
|
63
62
|
const verifiedRules = await captureProjectRules(snapshot.root, rulePaths, { signal: options.signal, files: snapshot.files });
|
|
64
63
|
if (JSON.stringify(verifiedRules) !== JSON.stringify(rules)) throw new Error('Rules changed during capture');
|
|
65
64
|
const result = { ...snapshot, context, ...(autoContext ? { autoContext } : {}), ...(rules.length ? { rules } : {}) };
|
package/src/tracked-context.mjs
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
import { xml } from './svn.mjs';
|
|
1
2
|
import { checked } from './process.mjs';
|
|
2
3
|
import { relativePath } from './content.mjs';
|
|
4
|
+
export async function svnContextIndex(root, options = {}) {
|
|
5
|
+
const bytes = await checked('svn',['--non-interactive','status','--xml','--verbose','--ignore-externals','--','.@'],{...options,cwd:root,maxBytes:1024*1024});
|
|
6
|
+
const entries=(xml(bytes).status?.target ?? []).flatMap(t=>t.entry ?? []);
|
|
7
|
+
if(entries.length>10000)throw new Error('Tracked context index limit exceeded');
|
|
8
|
+
const paths=[], seen=new Set();
|
|
9
|
+
for(const e of entries){const name=e['@_path'].replaceAll('\\','/');if(name==='.')continue;relativePath(name);if(seen.has(name))throw new Error('Duplicate SVN context path');seen.add(name);const s=e['wc-status'];if(s?.['@_item']==='normal'&&s['@_props']==='none'&&!['@_switched','@_copied','@_file-external','@_tree-conflicted'].some(k=>s[k]==='true'))paths.push(name);}
|
|
10
|
+
// SVN may reorder XML attributes; compare canonical parsed state, not serialization.
|
|
11
|
+
const canonical=v=>Array.isArray(v)?v.map(canonical):v&&typeof v==='object'?Object.fromEntries(Object.keys(v).sort().map(k=>[k,canonical(v[k])])):v;
|
|
12
|
+
return {paths:paths.sort(),fingerprint:JSON.stringify(canonical([...entries].sort((a,b)=>a['@_path']<b['@_path']?-1:a['@_path']>b['@_path']?1:0)))};
|
|
13
|
+
}
|
|
3
14
|
// Index metadata only; never invokes network, diff drivers or source reads.
|
|
4
15
|
export async function gitContextIndex(root, options = {}) {
|
|
5
16
|
const bytes = await checked('git', ['--no-optional-locks','-c','core.fsmonitor=false','-c','core.untrackedCache=false','ls-files','--stage','-z'], {...options,cwd:root,maxBytes:1024*1024});
|