@feng0531/dsh-code-review 0.38.0 → 0.40.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.
@@ -0,0 +1,44 @@
1
+ import {contextCandidates} from './context-candidates.mjs';
2
+ import {gitContextIndex} from './tracked-context.mjs';
3
+ import {readLocal} from './content.mjs';
4
+ // Fixed-depth literal navigation, not symbol binding. Never enumerate the repository.
5
+ export async function captureDependencyChain(root,files,options={}){
6
+ const signal=AbortSignal.any([AbortSignal.timeout(30000),...(options.signal?[options.signal]:[])]);
7
+ const scoped={...options,maxFileBytes:Math.min(options.maxFileBytes??256*1024,256*1024),signal},probes=new Set(),visited=new Set(files.map(f=>f.path)),context=[],candidates=[];
8
+ const reserved=new Set([...(options.contextPaths??[]),...(options.rulePaths??[])]),limit=Math.min(options.maxContextFiles??20,20);
9
+ let frontier=[...files],index,bytes=0,truncated=false,depth=0;
10
+ const explicitSeeds=[];
11
+ if(new Set(options.contextPaths??[]).size>limit)throw new Error('Context file limit exceeded');
12
+ for(const name of [...new Set(options.contextPaths??[])].sort()){
13
+ const changed=files.find(f=>f.path===name);
14
+ if(changed){if(changed.eligibility!=='reviewable'||changed.rightExists===false)throw new Error('Context path is an excluded blocked or deleted change');continue;}
15
+ const item={path:name,...await readLocal(root,name,scoped)};
16
+ bytes+=item.bytes;if(bytes>1024*1024)throw new Error('Dependency source byte limit exceeded');
17
+ explicitSeeds.push(item);visited.add(name);frontier.push({path:name,eligibility:'reviewable',right:item});
18
+ }
19
+ // Seeds are already explicitly selected, but descendants must still pass exact tracked probes.
20
+
21
+ const onProbe=paths=>{for(const p of paths)probes.add(p);if(probes.size>512)throw new Error('Git context lookup limit exceeded');};
22
+ for(depth=1;depth<=3&&frontier.length;depth++){
23
+ signal.throwIfAborted();for(let i=0;i<frontier.length;i+=200)contextCandidates(frontier.slice(i,i+200),[],{onProbe});
24
+ index=await gitContextIndex(root,{...scoped,probePaths:[...probes]});
25
+ const plans=[];for(let i=0;i<frontier.length;i+=200)plans.push(contextCandidates(frontier.slice(i,i+200),index.paths));const plan={candidates:plans.flatMap(p=>p.candidates),truncated:plans.some(p=>p.truncated)};truncated ||= plan.truncated;
26
+ const next=[];
27
+ for(const entry of plan.candidates){
28
+ if(visited.has(entry.path)||reserved.has(entry.path))continue;
29
+ visited.add(entry.path);const candidate={...entry,depth};candidates.push(candidate);
30
+ if(candidates.length>20)throw new Error('Dependency candidate limit exceeded');
31
+ if(context.length+new Set(options.contextPaths??[]).size>=limit){candidate.status='blocked';candidate.reason='Context file limit exceeded';continue;}
32
+ try{
33
+ const item={path:entry.path,...await readLocal(root,entry.path,scoped)};
34
+ bytes+=item.bytes;if(bytes>1024*1024)throw new Error('Dependency source byte limit exceeded');
35
+ context.push(item);candidate.status='captured';
36
+ next.push({path:item.path,eligibility:'reviewable',right:item});
37
+ }catch(error){signal.throwIfAborted();if(error.message==='Dependency source byte limit exceeded')throw error;candidate.status='blocked';candidate.reason=error.message;}
38
+ }
39
+ frontier=next;
40
+ }
41
+ if(frontier.length)truncated=true;
42
+ const indexOptions={...scoped,probePaths:[...probes]};
43
+ return {context,index,indexOptions,explicitSeeds,autoContext:{candidates,capturedPaths:context.map(c=>c.path),truncated,maxDepth:3,probeCount:probes.size,sourceBytes:bytes,notice:'Bounded current-side literal dependency navigation from changed and explicit context sources, depth <=3, 512 shared exact probes, 20 candidates/context slots, 1MiB source bytes and 30s through verification. Not symbol binding, caller coverage or complete definitions.'}};
44
+ }
package/src/snapshot.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import {captureDependencyChain} from './dependency-chain.mjs';
1
2
  import {captureSvnCallers} from './svn-caller-capture.mjs';
2
3
  import { captureCallers } from './caller-capture.mjs';
3
4
  import { validateCallerScopes } from './caller-index.mjs';
@@ -48,8 +49,11 @@ export async function captureSnapshot(cwd, options = {}) {
48
49
  if (!Array.isArray(rulePaths) || rulePaths.some(name => contextPaths.includes(name))) throw new Error('Rule paths must be separate from context paths');
49
50
  const rules = await captureProjectRules(snapshot.root, rulePaths, { signal: options.signal, files: snapshot.files });
50
51
  const context = [];
51
- let autoContext, index, indexOptions = options;
52
- if (options.autoContext === true) {
52
+ let autoContext, index, explicitSeeds = [], indexOptions = options;
53
+ if (options.autoContext === true && type==='git') {
54
+ const chain=await captureDependencyChain(snapshot.root,snapshot.files,options);
55
+ explicitSeeds=chain.explicitSeeds;context.push(...chain.context);autoContext=chain.autoContext;index=chain.index;indexOptions=chain.indexOptions;
56
+ } else if (options.autoContext === true) {
53
57
  if(type==='git'){const probes=new Set();contextCandidates(snapshot.files,[],{onProbe:paths=>{for(const p of paths)probes.add(p);if(probes.size>512)throw new Error('Git context lookup limit exceeded');}});indexOptions={...options,probePaths:[...probes]};}
54
58
  index = await (type === 'git' ? gitContextIndex : svnContextIndex)(snapshot.root, indexOptions);
55
59
  const plan = contextCandidates(snapshot.files, index.paths);
@@ -68,7 +72,12 @@ export async function captureSnapshot(cwd, options = {}) {
68
72
  if (changed.eligibility !== 'reviewable') throw new Error('Context path is an excluded or blocked change');
69
73
  if (changed.rightExists === false) throw new Error('Deleted path cannot be working context');
70
74
  context.push({ path: name, ...changed.right });
71
- } else context.push({ path: name, ...await readLocal(snapshot.root, name, options) });
75
+ } else {
76
+ const item={path:name,...await readLocal(snapshot.root,name,indexOptions)};
77
+ const seed=explicitSeeds.find(c=>c.path===name);
78
+ if(seed&&seed.hash!==item.hash)throw new Error('Explicit dependency seed changed during capture');
79
+ context.push(item);
80
+ }
72
81
  }
73
82
  let callerDiscovery, verifyCallers;
74
83
  if (options.callerScopePaths !== undefined) {
@@ -89,8 +98,8 @@ export async function captureSnapshot(cwd, options = {}) {
89
98
  }
90
99
  }
91
100
  // Re-capture verifies both selected sides while collecting explicit context.
92
- if ((await capture(cwd, options)).id !== snapshot.id) throw new Error('Snapshot changed while collecting context');
93
- for (const item of context) if ((await readLocal(snapshot.root, item.path, options)).hash !== item.hash) throw new Error('Context changed during capture');
101
+ if ((await capture(cwd, indexOptions)).id !== snapshot.id) throw new Error('Snapshot changed while collecting context');
102
+ for (const item of context) if ((await readLocal(snapshot.root, item.path, indexOptions)).hash !== item.hash) throw new Error('Context changed during capture');
94
103
  if (index && (await (type === 'git' ? gitContextIndex : svnContextIndex)(snapshot.root, indexOptions)).fingerprint !== index.fingerprint) throw new Error('Tracked context index changed during capture');
95
104
  const verifiedRules = await captureProjectRules(snapshot.root, rulePaths, { signal: options.signal, files: snapshot.files });
96
105
  if (JSON.stringify(verifiedRules) !== JSON.stringify(rules)) throw new Error('Rules changed during capture');