@feng0531/dsh-code-review 0.35.0 → 0.36.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,9 +1,14 @@
1
1
  # 变更记录
2
2
 
3
+ ## 0.36.0
4
+
5
+ - callerScopePaths支持Git固定commit/range两侧对象内有界字面引用发现;读取匹配/非匹配blob并绑定OID/hash/version与索引/竞争探针,execute重捕获;不checkout/fetch/读取工作树,SVN仍不支持。
6
+ - 两侧共用30秒、128正文/1MiB、256记录/64KiB及512探针/1MiB;共享20上下文/4MiB快照,oldOnly复用不自动批准new侧;根提交、删除、竞争歧义、共享扫描耗尽及预览执行已验收。导航不证明调用/语义绑定或完整覆盖,真实模型质量未验收。
7
+
3
8
  ## 0.35.0
4
9
 
5
10
  - 自动历史旧侧直接依赖导航命中已批准删除时复用原changed-old身份,excluded/blocked不能重新作为context进入,不多占上下文槽;共享原512探针、20候选/上下文、blob/快照字节限额,无checkout/fetch/工作树读取。
6
- - 普通变更继续走原变化身份,不新增重复正文;显式oldContext去重,满上下文槽不阻塞已批准旧身份复用。导航不证明语义调用/绑定,B2完整能力仍需后续来源场景。363离线验收通过,待冻结远端门禁。
11
+ - 普通变更继续走原变化身份,不新增重复正文;显式oldContext去重,满上下文槽不阻塞已批准旧身份复用。导航不证明语义调用/绑定,B2完整能力仍需后续来源场景。363离线验收、固定标签四矩阵及唯一发布37918056608通过;原artifact11610243325的92968字节与npm/Release逐字节、双hash及校验清单一致。
7
12
 
8
13
  ## 0.34.1
9
14
 
package/README.md CHANGED
@@ -4,11 +4,11 @@
4
4
 
5
5
  ## 当前状态
6
6
 
7
- 当前正式版本为 0.34.1(固定标签四矩阵及npm/Release原工件字节双hash门禁通过)。JSON围栏兼容与有界窗口上下文已交付;实际项目审查效果仍需原报告和新预览执行验证。
7
+ 当前正式版本为 0.35.0(固定标签四矩阵及npm/Release原工件字节双hash门禁通过)。JSON围栏兼容与有界窗口上下文已交付;实际项目审查效果仍需原报告和新预览执行验证。
8
8
 
9
9
  Git历史预览新增oldContextPaths:最多20个显式仓库相对文件,与contextPaths/自动上下文共享20槽、256KiB每blob及4MiB快照。仅从baseline普通Git blob捕获,不checkout/fetch/读取工作树;根提交无baseline拒绝,working/SVN明确拒绝。旧侧条目携带oldRevision/oldBlobOid/oldHash,仅进入context-old;oldOnly表示仅批准旧侧,不证明target路径不存在。所选变更复用原changed-old,不重复上下文;excluded/blocked变更、链接/submodule、凭据路径不能重新进入。预览显示身份元数据、不显示正文,确认执行才按宿主正常权限发送。主审/综合/复核按侧提示,计入原输入预算,旧证据不能冒充新侧回归证明。
10
10
 
11
- 预览可选callerScopePaths,最多4个非根、非重叠相对目录,仅Git工作区。预览读取目录内tracked JS/TS/C-family候选正文(含非匹配),仅匹配引用方作为上下文发送模型;128扫描文件/1MiB正文/256索引记录/64KiB索引/512竞争路径探针,发现与重核共享30秒,复核累计2MiB,仍共享20上下文及4MiB快照。绑定所有扫描hash、索引及竞争探针;execute重新捕获。历史/SVN明确不支持,不静默忽略。字面引用仅导航,不证明函数调用或完整覆盖,报告保留partial限制。
11
+ 预览可选callerScopePaths,最多4个非根、非重叠相对目录,支持Git工作区及固定commit/range两侧对象;SVN仍不支持。历史两侧共用30秒、128正文/1MiB、256记录/64KiB、512竞争探针/1MiB,blob版本/OID/hash及非匹配身份绑定;不读取工作树。预览读取目录内tracked JS/TS/C-family候选正文(含非匹配),仅匹配引用方作为上下文发送模型;128扫描文件/1MiB正文/256索引记录/64KiB索引/512竞争路径探针,发现与重核共享30秒,复核累计2MiB,仍共享20上下文及4MiB快照。绑定所有扫描hash、索引及竞争探针;execute重新捕获。SVN明确不支持,不静默忽略;历史root无旧侧,删除仅旧侧导航,oldOnly上下文复用不新增new授权。字面引用仅导航,不证明函数调用或完整覆盖,报告保留partial限制。
12
12
 
13
13
  最终报告记录业务需求hash/bytes用于溯源,不重复需求正文;真实Git当前/历史捕获及服务预览执行预算集成已验收。
14
14
 
@@ -30,7 +30,7 @@ C#自动上下文支持窄语法同目录Type.cs静态零参调用探针,需fi
30
30
 
31
31
  历史捕获用diff-tree精确端点变更元数据,不前置完整枚举文件树;显式上下文/规则和直接导入依赖按字面非递归路径查询。自动依赖最多512个待查路径,仍保留200变更/20上下文/4MiB快照限制,大树小提交不因无关文件元数据超限。
32
32
 
33
- 历史自动上下文也导航旧侧import/include,仅未修改且两侧普通blob;上下文oldText/oldRevision和目标revision明确,检索context-old单独身份,仍共用20文件总量及4MiB快照。旧侧独有/删除上下文暂不捕获,调用者覆盖仍不保证。
33
+ 历史自动上下文导航旧侧import/include:未修改依赖保留两侧普通blob及版本;已批准删除依赖复用原changed-old,不重复捕获或占用上下文槽。excluded/blocked删除不恢复,目标链接不赋予新侧权限。新增上下文仍共享20槽、blob字节及4MiB快照限额,自动来源边界其余场景待验收;导航不证明调用者覆盖或符号绑定。
34
34
 
35
35
  单提交审查直接读取原始commit头的父节点,不受浅克隆遍历边界影响;缺失父树明确失败且不自动fetch,不能把非根提交当空旧侧。
36
36
 
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 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. Preview each repository separately. Show the preview before execution.',
15
- parameters: { type: 'object', additionalProperties: false, properties: { 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 historical baseline-only blob context paths. Shares 20 context slots. No target text is inferred; selected old changes reuse their original identity, excluded changes cannot reenter. Unsupported on working reviews and SVN.' }, 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 working review 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. Context is captured from target tree only; explicit rules are target-tree blobs too.' }, 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 } } },
15
+ parameters: { type: 'object', additionalProperties: false, properties: { 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 historical baseline-only blob context paths. Shares 20 context slots. No target text is inferred; selected old changes reuse their original identity, excluded changes cannot reenter. Unsupported on working reviews and SVN.' }, 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 working or fixed commit/range review; SVN unsupported. 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feng0531/dsh-code-review",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "private": false,
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
@@ -1,3 +1,4 @@
1
+ import {captureHistoryCallers} from './history-caller-capture.mjs';
1
2
  import { isSecretPath, assertNonSecretPath } from './secret-path.mjs';
2
3
  import {captureProjectRules} from './project-rules.mjs';
3
4
  import {contextCandidates} from './context-candidates.mjs';
@@ -52,5 +53,7 @@ export async function captureGitHistory(cwd, options={}) {
52
53
  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));}});
53
54
  let autoContext;
54
55
  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.keys()]);const oldPlan=contextCandidates(files.map(f=>f.eligibility==='reviewable'?{...f,right:f.left,rightExists:true}:f),[...left.keys()],{includeChanged:true});const combined=new Map(plan.candidates.map(c=>[c.path,{...c,sourceSides:['new']}]));for(const c of oldPlan.candidates){if(files.some(f=>f.path===c.path)&&['100644','100755'].includes(right.get(c.path)?.mode))continue;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)&&!oldContextPaths.includes(c.path)&&!rulePaths.includes(c.path)),capturedPaths:[]};for(const c of autoContext.candidates){signal?.throwIfAborted();try{const changed=files.find(f=>f.path===c.path);if(c.sourceSides.length===1&&c.sourceSides[0]==='old'&&!regular(right.get(c.path))){if(changed){if(changed.eligibility!=='reviewable'||!changed.left||!regular(left.get(c.path)))throw new Error('Old dependency conflicts with excluded blocked or absent old change');c.status='reused-changed-old';continue;}if(context.length>=maxContextFiles)throw new Error('Context file limit exceeded');const entry=left.get(c.path);if(!regular(entry))throw new Error('Old dependency not a baseline regular blob');const content=decode(await git(root,['cat-file','blob',entry.oid],maxFileBytes));context.push({path:c.path,oldOnly:true,oldText:content.text,oldHash:hash(content.text),oldBlobOid:entry.oid,oldRevision:base});c.status='captured';autoContext.capturedPaths.push(c.path);continue;}if(context.length>=maxContextFiles)throw new Error('Context file limit exceeded');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;}}}
55
- 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)};
56
+ let callerDiscovery;
57
+ if(options.callerScopePaths!==undefined){const callers=await captureHistoryCallers(root,files,{base,target},options.callerScopePaths,options);callerDiscovery=callers.discovery;for(const item of callers.context){const matches=callerDiscovery.candidates.filter(c=>c.fromPath===item.path),present=context.find(c=>c.path===item.path);if(present){if((present.text!==undefined&&present.text!==item.text)||(present.oldText!==undefined&&present.oldText!==item.oldText))throw new Error('Historical caller context identity conflict');for(const c of matches){const available=c.side==='old'?present.oldText!==undefined:present.text!==undefined;c.status=available?'reused-context':'blocked';if(!available)c.captureReason='Existing context did not approve this version side';}continue;}if(context.length>=maxContextFiles){for(const c of matches){c.status='blocked';c.captureReason='Context file limit exceeded';}continue;}context.push(item);callerDiscovery.capturedPaths.push(item.path);for(const c of matches)c.status='captured';}}
58
+ const snapshot={context,...(callerDiscovery?{callerDiscovery}:{}),...(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)};
56
59
  }
@@ -0,0 +1,15 @@
1
+ import {historyCallerIndex,historyCallerProbes} from './history-caller-index.mjs';
2
+ import {callerCandidates} from './caller-candidates.mjs';
3
+ import {checked} from './process.mjs';
4
+ import {decode,hash} from './content.mjs';
5
+ import {isSecretPath} from './secret-path.mjs';
6
+ // Both sides share one capture lifecycle and all discovery limits. Never called by model readers.
7
+ export async function captureHistoryCallers(root,files,history,scopes,options={}){
8
+ const deadline=AbortSignal.timeout(30000),signal=options.signal?AbortSignal.any([options.signal,deadline]):deadline,bounded={...options,signal};
9
+ const changed=new Set(files.map(f=>f.path)),rules=new Set(options.rulePaths??[]),scanned=[],candidates=[],indexes=[],contexts=new Map();let scanBytes=0,indexBytes=0,indexRecords=0,probeBytes=0;const allProbes=new Set();
10
+ for(const [side,commit] of [['old',history.base],['new',history.target]]){if(!commit)continue;const index=await historyCallerIndex(root,commit,scopes,bounded);indexBytes+=index.bytes;indexRecords+=index.records;if(indexBytes>64*1024||indexRecords>256)throw new Error('Historical caller shared index limit exceeded');const sources=[];
11
+ for(const entry of index.entries){signal.throwIfAborted();if(changed.has(entry.path)||rules.has(entry.path)||isSecretPath(entry.path)||!/\.(mjs|cjs|js|jsx|ts|tsx|c|cc|cpp|cxx|h|hh|hpp|hxx|shader|hlsl|glsl|cginc|hlsli|compute)$/i.test(entry.path))continue;if(scanned.length>=128)throw new Error('Historical caller shared scan file limit exceeded');const content=decode(await checked('git',['--no-optional-locks','cat-file','blob',entry.oid],{...bounded,cwd:root,maxBytes:Math.min(options.maxFileBytes??256*1024,256*1024)}));scanBytes+=content.bytes;if(scanBytes>1024*1024)throw new Error('Historical caller shared scan byte limit exceeded');const source={path:entry.path,...content,blobOid:entry.oid,revision:commit,side};scanned.push(source);sources.push(source);}
12
+ const targets=side==='old'?files.map(f=>({...f,rightExists:!!f.left})):files;const requested=callerCandidates(targets,sources,[]).probePaths;for(const p of requested)allProbes.add(p);if(allProbes.size>512)throw new Error('Historical caller shared probe limit exceeded');const probes=await historyCallerProbes(root,commit,requested,bounded);probeBytes+=probes.bytes;if(probeBytes>1024*1024)throw new Error('Historical caller shared probe byte limit exceeded');const plan=callerCandidates(targets,sources,probes.paths);indexes.push({side,commit,indexFingerprint:index.fingerprint,probeFingerprint:probes.fingerprint});for(const c of plan.candidates)candidates.push({...c,side,revision:commit});}
13
+ const wanted=new Set(candidates.map(c=>c.fromPath));for(const s of scanned){if(!wanted.has(s.path))continue;const item=contexts.get(s.path)??{path:s.path};if(s.side==='new')Object.assign(item,{text:s.text,hash:s.hash,bytes:s.bytes,encoding:s.encoding,revision:s.revision});else Object.assign(item,{oldText:s.text,oldHash:hash(s.text),oldBlobOid:s.blobOid,oldRevision:s.revision});contexts.set(s.path,item);}for(const item of contexts.values())if(item.text===undefined)item.oldOnly=true;signal.throwIfAborted();
14
+ return {context:[...contexts.values()].sort((a,b)=>a.path<b.path?-1:1),discovery:{scopes,history,indexes,scanned:scanned.map(({path,hash,bytes,blobOid,revision,side})=>({path,hash,bytes,blobOid,revision,side})),scannedBytes:scanBytes,candidates,capturedPaths:[],incomplete:true,notice:'Fixed baseline/target literal references in explicit scopes only; no worktree reads, semantic binding or complete caller coverage.',limitations:['Only supported literal references within approved scopes were searched.']}};
15
+ }
@@ -0,0 +1,11 @@
1
+ import {checked} from './process.mjs';
2
+ import {relativePath,hash} from './content.mjs';
3
+ import {validateCallerScopes} from './caller-index.mjs';
4
+ const oidPattern=/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
5
+ function revision(value){if(typeof value!=='string'||!oidPattern.test(value))throw new Error('Historical caller index requires fixed commit OID');}
6
+ const git=(root,args,options)=>checked('git',['--no-optional-locks','--literal-pathspecs',...args],{...options,cwd:root,maxBytes:64*1024});
7
+ function records(chunk){const text=new TextDecoder('utf-8',{fatal:true}).decode(chunk);if(text&&!text.endsWith('\0'))throw new Error('Incomplete historical caller index');return text.split('\0').filter(Boolean).map(record=>{const m=/^(\d{6}) (blob|tree|commit) ([a-f0-9]{40}|[a-f0-9]{64})\t([\s\S]+)$/.exec(record);if(!m)throw new Error('Invalid historical caller record');return {mode:m[1],type:m[2],oid:m[3],path:relativePath(m[4])};});}
8
+ // Directory scopes only. No complete tree enumeration and no worktree/index reads.
9
+ export async function historyCallerIndex(root,commit,scopes,options={}){revision(commit);const approved=validateCallerScopes(scopes),chunks=[],entries=[];let bytes=0,count=0;for(const scope of approved){const chunk=await git(root,['ls-tree','-r','-z',commit,'--',scope],options);bytes+=chunk.length;if(bytes>64*1024)throw new Error('Historical caller index byte limit exceeded');for(const entry of records(chunk)){if(++count>256)throw new Error('Historical caller index path limit exceeded');if(!entry.path.startsWith(scope+'/'))throw new Error('Historical caller scope is not a directory or escaped scope');entries.push(entry);}chunks.push(Buffer.from(scope+'\0'),chunk);}return {commit,scopes:approved,entries:entries.filter(e=>e.type==='blob'&&['100644','100755'].includes(e.mode)).sort((a,b)=>a.path<b.path?-1:1),fingerprint:hash(Buffer.concat([Buffer.from(commit+'\0'),...chunks])),records:count,bytes};}
10
+ // A tree, link or submodule remains a resolution competitor, never a body grant.
11
+ export async function historyCallerProbes(root,commit,paths,options={}){revision(commit);if(!Array.isArray(paths)||paths.length>512||new Set(paths).size!==paths.length)throw new Error('Invalid historical caller probes');const chunks=[Buffer.from(commit+'\0')],existing=[];let bytes=0;for(const name of [...paths].sort()){relativePath(name);const chunk=await git(root,['ls-tree','-z',commit,'--',name],options);bytes+=chunk.length;if(bytes>1024*1024)throw new Error('Historical caller probe metadata limit exceeded');const found=records(chunk);if(found.some(e=>e.path!==name))throw new Error('Historical caller probe escaped exact path');if(found.length)existing.push(name);chunks.push(Buffer.from(name+'\0'),chunk);}return {commit,paths:existing,fingerprint:hash(Buffer.concat(chunks)),bytes};}
package/src/snapshot.mjs CHANGED
@@ -38,7 +38,7 @@ export async function captureSnapshot(cwd, options = {}) {
38
38
  for (const name of options.oldContextPaths ?? []) relativePath(name);
39
39
  const type = await detectVcs(cwd);
40
40
  if (options.oldContextPaths !== undefined && (type !== 'git' || !['commit','baseRevision','targetRevision'].some(k => options[k] !== undefined))) throw new Error('Old context requires Git historical review');
41
- if (options.callerScopePaths !== undefined && (type !== 'git' || ['commit','baseRevision','targetRevision'].some(k => options[k] !== undefined))) throw new Error('Caller discovery currently requires Git working review');
41
+ if (options.callerScopePaths !== undefined && type !== 'git') throw new Error('Caller discovery currently requires Git review');
42
42
  if (['commit','baseRevision','targetRevision'].some(k => options[k] !== undefined)) {
43
43
  if (type !== 'git') throw new Error('Git revision review is unsupported on SVN');
44
44
  return bindBusinessRequirement(await captureGitHistory(cwd, options),options.businessRequirement);