@feng0531/dsh-code-review 0.24.1 → 0.25.1
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 +11 -0
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/context-candidates.mjs +2 -1
- package/src/git-history.mjs +12 -4
- package/src/review.mjs +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.25.1
|
|
4
|
+
|
|
5
|
+
- 修复大仓库小提交历史快照Command output limit exceeded:用diff-tree raw NUL元数据取代两端全树ls-tree,不扩大整树上限。
|
|
6
|
+
- 上下文/规则局部字面非递归对象查询,自动依赖512路径限,禁目录/通配符扩张;无工作树读取或fetch。
|
|
7
|
+
- 真实11000文件超1MiB树的4 Shader提交回归、自动上下文规则及安全边界通过;全量266通过。
|
|
8
|
+
|
|
9
|
+
## 0.25.0
|
|
10
|
+
|
|
11
|
+
- 跨文件交互批次也制定风险假设,至少两文件精确editRefs,同一主调用signal及全局预算,不冒充证据。
|
|
12
|
+
- 实际生成输入/单文件假设拒绝/调用耗尽回归,业务组集成更新额外规划调用。
|
|
13
|
+
|
|
3
14
|
## 0.24.1
|
|
4
15
|
|
|
5
16
|
- 历史上下文关系分别对基线/目标文本推断,标记contextSide/contextVersion;缺少oldText不回退目标正文。
|
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
## 当前状态
|
|
6
6
|
|
|
7
|
-
当前代码版本为 0.
|
|
7
|
+
当前代码版本为 0.25.1
|
|
8
|
+
|
|
9
|
+
历史捕获用diff-tree精确端点变更元数据,不前置完整枚举文件树;显式上下文/规则和直接导入依赖按字面非递归路径查询。自动依赖最多512个待查路径,仍保留200变更/20上下文/4MiB快照限制,大树小提交不因无关文件元数据超限。
|
|
8
10
|
|
|
9
11
|
历史自动上下文也导航旧侧import/include,仅未修改且两侧普通blob;上下文oldText/oldRevision和目标revision明确,检索context-old单独身份,仍共用20文件总量及4MiB快照。旧侧独有/删除上下文暂不捕获,调用者覆盖仍不保证。
|
|
10
12
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import {relativePath} from './content.mjs';
|
|
3
3
|
// Navigation only over caller-supplied tracked regular paths; no reads or authority.
|
|
4
|
-
export function contextCandidates(files, trackedPaths, {maxCandidates=20}={}) {
|
|
4
|
+
export function contextCandidates(files, trackedPaths, {maxCandidates=20,onProbe}={}) {
|
|
5
5
|
if(!Array.isArray(files)||files.length>200||!Array.isArray(trackedPaths)||trackedPaths.length>10000||!Number.isSafeInteger(maxCandidates)||maxCandidates<1||maxCandidates>20)throw new Error('Invalid context candidate limit');
|
|
6
6
|
const tracked=new Set();for(const p of trackedPaths){relativePath(p);if(tracked.has(p))throw new Error('Duplicate tracked path');tracked.add(p);}
|
|
7
7
|
const changed=new Set(files.flatMap(f=>[f.path,...(f.oldPath?[f.oldPath]:[])]));
|
|
@@ -17,6 +17,7 @@ export function contextCandidates(files, trackedPaths, {maxCandidates=20}={}) {
|
|
|
17
17
|
if(!match)continue;const spec=cpp?match[1]:match[2];if(!cpp&&!/^\.\.?\//.test(spec))continue;
|
|
18
18
|
const target=path.posix.normalize(path.posix.join(path.posix.dirname(f.path),spec));try{relativePath(target);}catch{continue;}
|
|
19
19
|
const candidates=js&&!path.posix.extname(target)?[target,...['.mjs','.cjs','.js','.jsx','.ts','.tsx'].flatMap(e=>[target+e,target+'/index'+e])]:[target];
|
|
20
|
+
onProbe?.(candidates);
|
|
20
21
|
const matches=candidates.filter(p=>tracked.has(p));if(matches.length===1)add(matches[0],f.path,cpp?'literal-relative-include':'literal-relative-import');
|
|
21
22
|
}
|
|
22
23
|
}
|
package/src/git-history.mjs
CHANGED
|
@@ -15,20 +15,28 @@ export async function captureGitHistory(cwd, options={}) {
|
|
|
15
15
|
let base,target;
|
|
16
16
|
if(commit!==undefined){if(baseRevision!==undefined||targetRevision!==undefined)throw new Error('Commit and range are mutually exclusive');target=await resolve(commit);const object=utf8(await git(root,['cat-file','commit',target]));const header=object.split('\n\n',1)[0];const parents=header.split('\n').filter(line=>line.startsWith('parent ')).map(line=>line.slice(7));if(parents.some(oid=>!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(oid)))throw new Error('Invalid commit parent');if(parents.length>1)throw new Error('Merge commit requires explicit revision range');base=parents[0]??null;}
|
|
17
17
|
else {base=await resolve(baseRevision);target=await resolve(targetRevision);}
|
|
18
|
-
|
|
19
|
-
const
|
|
18
|
+
// Metadata scales with changed paths, not every tracked file in either tree.
|
|
19
|
+
const raw=utf8(await git(root,['diff-tree','--no-abbrev','--raw','-z','-r','--no-renames','--no-ext-diff','--no-textconv','--no-commit-id',...(base?[base,target]:['--root',target]),'--'])).split('\0');
|
|
20
|
+
if(raw.at(-1)==='')raw.pop();
|
|
21
|
+
const left=new Map(),right=new Map(),names=[];
|
|
22
|
+
if(raw.length%2)throw new Error('Invalid historical diff metadata');
|
|
23
|
+
for(let i=0;i<raw.length;i+=2){const m=/^:(\d{6}) (\d{6}) ([a-f0-9]{40}|[a-f0-9]{64}) ([a-f0-9]{40}|[a-f0-9]{64}) ([AMDT])$/.exec(raw[i]);if(!m)throw new Error('Invalid historical diff metadata');const name=relativePath(raw[i+1]);if(names.includes(name))throw new Error('Duplicate historical change');names.push(name);for(const [map,mode,oid] of [[left,m[1],m[3]],[right,m[2],m[4]]])if(mode!=='000000')map.set(name,{mode,type:mode==='160000'?'commit':'blob',oid});}
|
|
24
|
+
names.sort();
|
|
25
|
+
// Literal, nonrecursive lookups cannot enumerate a supplied directory or wildcard.
|
|
26
|
+
const lookup=async (oid,map,requested)=>{if(!oid)return;const pending=[...new Set(requested)].filter(p=>!map.has(p));for(let i=0;i<pending.length;i+=32){const paths=pending.slice(i,i+32);paths.forEach(relativePath);const records=utf8(await git(root,['--literal-pathspecs','ls-tree','-z','--full-tree',oid,'--',...paths])).split('\0').filter(Boolean);for(const r of records){const m=/^(\d{6}) (blob|commit|tree) ([a-f0-9]+)\t([\s\S]+)$/.exec(r);if(!m||!paths.includes(m[4]))throw new Error('Invalid historical path lookup');if(m[2]!=='tree')map.set(m[4],{mode:m[1],type:m[2],oid:m[3]});}}};
|
|
20
27
|
if(names.length>maxFiles)throw new Error('Historical change count limit exceeded');if(selectedPaths?.some(p=>!names.includes(p)))throw new Error('Selected path is not a historical change');
|
|
21
28
|
const files=[];for(const name of names){signal?.throwIfAborted();const l=left.get(name),r=right.get(name),item={id:hash(name),path:name,rawStatus:!l?'A':!r?'D':'M',eligibility:'reviewable',properties:[]};files.push(item);if(selectedPaths&&!selectedPaths.includes(name)){item.eligibility='excluded';item.reason='Not explicitly selected';continue;}try{for(const e of [l,r].filter(Boolean))if(e.type!=='blob'||!['100644','100755'].includes(e.mode))throw new Error('Historical link or submodule excluded');const read=async e=>decode(e?await git(root,['cat-file','blob',e.oid],maxFileBytes):Buffer.alloc(0));item.left=await read(l);item.right=await read(r);item.rightExists=!!r;if(l&&r&&item.left.hash===item.right.hash){item.eligibility='excluded';item.reason='Mode-only historical change not analyzed';}}catch(error){signal?.throwIfAborted();item.eligibility='blocked';item.reason=error.message;delete item.left;delete item.right;}if(Buffer.byteLength(JSON.stringify(files))>maxSnapshotBytes)throw new Error('Historical snapshot limit exceeded');}
|
|
22
29
|
const rulePaths=options.rulePaths ?? [];
|
|
23
30
|
const contextPaths=options.contextPaths ?? [], maxContextFiles=options.maxContextFiles ?? 20;
|
|
24
31
|
if(!Array.isArray(contextPaths)||!Number.isSafeInteger(maxContextFiles)||maxContextFiles<1||maxContextFiles>20||contextPaths.length>maxContextFiles)throw new Error('Historical context file limit exceeded');
|
|
25
32
|
if(options.autoContext!==undefined&&typeof options.autoContext!=='boolean')throw new Error('Invalid autoContext option');
|
|
26
|
-
if(!Array.isArray(rulePaths)||rulePaths.some(p=>contextPaths.includes(p)))throw new Error('Rule paths must be separate from context paths');
|
|
33
|
+
if(!Array.isArray(rulePaths)||rulePaths.length>4||rulePaths.some(p=>contextPaths.includes(p)))throw new Error('Rule paths must be separate from context paths');
|
|
34
|
+
await lookup(target,right,[...contextPaths,...rulePaths]);await lookup(base,left,contextPaths);
|
|
27
35
|
const context=[], regular=e=>e?.type==='blob'&&['100644','100755'].includes(e.mode);
|
|
28
36
|
const captureContext=async name=>{relativePath(name);const changed=files.find(f=>f.path===name);if(changed){if(changed.eligibility!=='reviewable'||changed.rightExists===false)throw new Error('Historical context is excluded blocked or deleted');return {path:name,...changed.right};}const entry=right.get(name);if(!regular(entry))throw new Error('Historical context not a target regular blob');const content=decode(await git(root,['cat-file','blob',entry.oid],maxFileBytes));const previous=left.get(name);return {path:name,...content,revision:target,...(regular(previous)?{oldText:decode(await git(root,['cat-file','blob',previous.oid],maxFileBytes)).text,oldRevision:base}:{})};};
|
|
29
37
|
for(const name of [...new Set(contextPaths)].sort())context.push(await captureContext(name));
|
|
30
38
|
const rules=await captureProjectRules(root,rulePaths,{signal,files,read:async (_root,name,{maxFileBytes:limit})=>{const entry=right.get(name);if(!regular(entry))throw new Error('Historical rule not a target regular blob');return decode(await git(root,['cat-file','blob',entry.oid],limit));}});
|
|
31
39
|
let autoContext;
|
|
32
|
-
if(options.autoContext===true){const plan=contextCandidates(files,[...right].filter(([,e])=>regular(e)).map(([p])=>p));const oldPlan=contextCandidates(files.map(f=>f.eligibility==='reviewable'?{...f,right:f.left,rightExists:true}:f),[...left].filter(([p,e])=>regular(e)&®ular(right.get(p))&&!names.includes(p)).map(([p])=>p));const combined=new Map(plan.candidates.map(c=>[c.path,{...c,sourceSides:['new']}]));for(const c of oldPlan.candidates){if(combined.has(c.path))combined.get(c.path).sourceSides.push('old');else combined.set(c.path,{...c,sourceSides:['old']});}plan.candidates=[...combined.values()].sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);plan.truncated ||= oldPlan.truncated;autoContext={...plan,notice:'Direct baseline/target-tree import/include navigation only; no caller coverage, binding or working-tree reads.',target,base,candidates:plan.candidates.filter(c=>!contextPaths.includes(c.path)&&!rulePaths.includes(c.path)),capturedPaths:[]};for(const c of autoContext.candidates){signal?.throwIfAborted();if(context.length>=maxContextFiles){c.status='blocked';c.reason='Context file limit exceeded';continue;}try{context.push(await captureContext(c.path));c.status='captured';autoContext.capturedPaths.push(c.path);}catch(error){signal?.throwIfAborted();c.status='blocked';c.reason=error.message;}}}
|
|
40
|
+
if(options.autoContext===true){const probes=new Set();const oldFiles=files.map(f=>f.eligibility==='reviewable'?{...f,right:f.left,rightExists:true}:f);const onProbe=paths=>{for(const p of paths)probes.add(p);if(probes.size>512)throw new Error('Historical context lookup limit exceeded');};contextCandidates(files,[],{onProbe});contextCandidates(oldFiles,[],{onProbe});await lookup(target,right,[...probes]);await lookup(base,left,[...probes]);const plan=contextCandidates(files,[...right].filter(([,e])=>regular(e)).map(([p])=>p));const oldPlan=contextCandidates(files.map(f=>f.eligibility==='reviewable'?{...f,right:f.left,rightExists:true}:f),[...left].filter(([p,e])=>regular(e)&®ular(right.get(p))&&!names.includes(p)).map(([p])=>p));const combined=new Map(plan.candidates.map(c=>[c.path,{...c,sourceSides:['new']}]));for(const c of oldPlan.candidates){if(combined.has(c.path))combined.get(c.path).sourceSides.push('old');else combined.set(c.path,{...c,sourceSides:['old']});}plan.candidates=[...combined.values()].sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);plan.truncated ||= oldPlan.truncated;autoContext={...plan,notice:'Direct baseline/target-tree import/include navigation only; no caller coverage, binding or working-tree reads.',target,base,candidates:plan.candidates.filter(c=>!contextPaths.includes(c.path)&&!rulePaths.includes(c.path)),capturedPaths:[]};for(const c of autoContext.candidates){signal?.throwIfAborted();if(context.length>=maxContextFiles){c.status='blocked';c.reason='Context file limit exceeded';continue;}try{context.push(await captureContext(c.path));c.status='captured';autoContext.capturedPaths.push(c.path);}catch(error){signal?.throwIfAborted();c.status='blocked';c.reason=error.message;}}}
|
|
33
41
|
const snapshot={context,...(rules.length?{rules}:{}),...(autoContext?{autoContext}:{}),schemaVersion:1,vcs:'git',root,baseline:base,history:{mode:commit!==undefined?'commit':'range',base,target,semantics:'exact-tree-endpoints; renames represented as add/delete'},files};const serialized=JSON.stringify(snapshot);if(Buffer.byteLength(serialized)>maxSnapshotBytes)throw new Error('Historical snapshot limit exceeded');return {...snapshot,id:hash(serialized)};
|
|
34
42
|
}
|
package/src/review.mjs
CHANGED
|
@@ -157,12 +157,14 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
|
|
|
157
157
|
for (const batch of batches) {
|
|
158
158
|
enclosingRequest.signal.throwIfAborted();
|
|
159
159
|
let batchPayload = batch.payload;
|
|
160
|
-
if (options.enableRiskPlanning === true && !batch.synthesis
|
|
160
|
+
if (options.enableRiskPlanning === true && !batch.synthesis) {
|
|
161
161
|
state.riskPlans ??= [];
|
|
162
|
-
const entry = { status: 'pending', windowIds: batch.windowIds, risks: [] };
|
|
162
|
+
const entry = { status: 'pending', windowIds: batch.windowIds, ...(batch.interaction ? {groupId:batch.interaction.id,sourceMode:'file-interaction'} : {}), risks: [] };
|
|
163
163
|
state.riskPlans.push(entry);
|
|
164
164
|
const response = await executor({ ...enclosingRequest, input: batch.payload, instructions: RISK_PLAN_INSTRUCTIONS });
|
|
165
|
-
const
|
|
165
|
+
const riskFiles = batch.interaction ? JSON.parse(batch.payload).files : [{ fileId: file.id, edits: changes.edits ?? [] }];
|
|
166
|
+
const risks = validateRiskPlan(response, riskFiles, scope?.catalog() ?? []);
|
|
167
|
+
if (batch.interaction && risks.some(r=>new Set(r.editRefs.map(ref=>ref.fileId)).size<2)) throw new Error('Interaction risk requires edits across at least two files');
|
|
166
168
|
entry.status = 'completed'; entry.risks = risks;
|
|
167
169
|
batchPayload = JSON.stringify({ ...JSON.parse(batch.payload), riskPlan: risks, riskPlanNotice: '未可信假设而非发现或证据;须验证及寻找反证,不增加读取权限。' });
|
|
168
170
|
if (!initialInputBudget({ instructions: REVIEW_INSTRUCTIONS, input: batchPayload }, scope, maxInputBytes).fits) throw new Error('Risk-enriched review input exceeds budget');
|