@feng0531/dsh-code-review 0.40.0 → 0.41.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
@@ -2,9 +2,13 @@
2
2
 
3
3
  本文件介绍对使用者有意义的变化。安装与参数见 [README](README.md),本次发布正文见 [发布说明](RELEASE-NOTES.md)。历史提交和安装包保留在 [GitHub Releases](https://github.com/f-e-n-g-0531/dsh-code-review/releases);本文不把开发日志当作功能介绍。
4
4
 
5
+ ## 0.41.0 — Git 历史两侧依赖链
6
+
7
+ 启用 autoContext 后,提交和区间审查沿固定修改前后版本的静态依赖读取最多三层上下文。两侧共享预算;旧侧专用上下文保持旧侧权限,不读取工作区,不恢复排除或阻断内容。SVN 仍为直接导航。
8
+
5
9
  ## 0.40.0 — 手动上下文的依赖导航
6
10
 
7
- Git 工作区启用 autoContext 后,也沿 contextPaths 指定文件的静态依赖取得必要实现。手动与自动来源共享数量、正文和时间限制;不改变未启用时的行为,不把排除变更或未跟踪依赖重新纳入。历史与 SVN 仍仅直接导航。
11
+ Git 工作区启用 autoContext 后,也沿 contextPaths 指定文件的静态依赖取得必要实现。手动与自动来源共享数量、正文和时间限制;不改变未启用时的行为,不把排除变更或未跟踪依赖重新纳入。历史与 SVN 仍仅直接导航。已正式发布;388 项回归、实际宿主、独立安装及四组 CI 通过,公开安装包与保存原包一致。
8
12
 
9
13
  ## 0.39.1 — 文档整理
10
14
 
@@ -30,15 +30,15 @@
30
30
  | 5 | 剩余预算组合与端到端矩阵 | 分组、窗口、检索、多次审查的终态与覆盖一致 |
31
31
  | 后置 | 真实模型质量评测 | 保存真实输出,人工裁决,分开统计失败和未完成 |
32
32
 
33
- Git 工作区三层字面依赖链已实现并通过测试。这解决了“导入转发文件后,未取得后续实现正文”的部分问题,但仍不是完整定义绑定。本批还补齐了手动上下文作为链入口,真实 Git、服务与独立安装四项测试通过,388 项全量回归和实际宿主检查通过,待正式发布。历史与 SVN 不具备同样的链式能力。
33
+ Git 工作区三层字面依赖链已实现并通过测试。这解决了“导入转发文件后,未取得后续实现正文”的部分问题,但仍不是完整定义绑定。本批还补齐了手动上下文作为链入口,真实 Git、服务与独立安装四项测试通过,388 项全量回归和实际宿主检查通过,已随 v0.40.0 正式交付。Git 历史三层链本批已实现,5 项独立安装验收及 393 项回归、实际宿主检查通过,待固定版本正式发布;SVN 尚未具备同样的链式能力。
34
34
 
35
35
  ## 当前验证结论
36
36
 
37
- - 最近完整回归:388 项通过。
37
+ - 最近完整回归:393 项通过。
38
38
  - 实际 DSH 工具运行时检查通过;模型响应受控,不是质量成绩。
39
39
  - v0.39.0 独立安装包的真实 Git、服务与预算测试:5 项通过。
40
40
  - v0.39.1 已正式交付,包含 Git 工作区三层依赖能力。四组 CI 通过;npm、Release 与保存原包的 91,502 字节完全一致,SHA-256、SHA-512 和校验清单通过。v0.39.0 不单独计作已核验交付。
41
- - 文档重构与正式发布已完成。新增文档一致性检查通过;本表中的核心差距仍未完成。
41
+ - v0.40.0 已正式交付手动上下文依赖导航。四组 CI 与唯一发布通过,npm、Release 与保存原包的 91,901 字节完全一致,双哈希及校验清单通过。准确语义绑定、历史与 SVN 依赖链等差距仍保留。
42
42
 
43
43
  ## 不在范围内
44
44
 
package/DEVELOPMENT.md CHANGED
@@ -19,6 +19,7 @@
19
19
  - 每个主文件的模型与检索阶段共享默认 120 秒;关联综合沿用相关主文件的生命周期,不因切换阶段重新计时。
20
20
  - 每阶段最多三轮检索;全局最多 50 次读取请求,返回内容累计 256 KiB,单次最多 64 KiB,读取最多 200 行。
21
21
  - Git 工作区依赖链最多三层、512 个精确路径查询、20 个候选、手动入口与自动依赖共享 1 MiB 正文;文件读取与最终复核共享 30 秒。
22
+ - Git 历史依赖链同样最多三层,两侧共享 512 个路径、20 个候选和上下文、1 MiB 正文及 30 秒;只读取固定对象。
22
23
  - 引用方发现最多四个不重叠的非根目录;128 个候选正文共 1 MiB,目录索引最多 256 条/64 KiB,竞争路径最多 512 个。发现与复核共享 30 秒。
23
24
 
24
25
  达到上限必须阻断或说明未完成,不能静默扩大范围、丢弃证据或重新计时。
package/README.md CHANGED
@@ -67,7 +67,7 @@ DSH 的只读代码审查插件。它比较 Git 或 SVN 中的代码变更,使
67
67
 
68
68
  commit 与 baseRevision/targetRevision 不能同时使用。oldContextPaths 只能用于 Git 历史审查。
69
69
 
70
- 自动上下文和引用方发现是保守的文本导航,不是编译器分析。动态导入、宏、重载、继承和实例方法可能无法解析;找到引用不等于证明函数实际被调用。Git 历史与 SVN 的自动依赖捕获目前仍限于直接依赖。
70
+ 自动上下文和引用方发现是保守的文本导航,不是编译器分析。动态导入、宏、重载、继承和实例方法可能无法解析;找到引用不等于证明函数实际被调用。Git 历史可沿修改前后两侧分别捕获最多三层依赖;SVN 目前仍限直接依赖。
71
71
 
72
72
  ## 如何读报告
73
73
 
package/RELEASE-NOTES.md CHANGED
@@ -1,23 +1,22 @@
1
- # v0.40.0 发布说明
1
+ # v0.41.0 发布说明
2
2
 
3
- ## 新增:沿手动上下文取得必要实现
3
+ ## 新增:Git 历史依赖链
4
4
 
5
- 在 Git 工作区同时设置 contextPaths 和 autoContext: true 时,自动上下文现在也从手动选定的文件出发。若该文件导入或转发其他实现,插件最多沿三层静态依赖取得正文,帮助理解变更所需的类型与实现。
5
+ 审查 Git 提交或明确版本区间时,启用 autoContext 可以从修改前后源码分别沿静态导入和包含关系取得最多三层必要上下文。手动指定的历史上下文也可作为入口。
6
6
 
7
- 不启用 autoContext 时,手动上下文仍只包含指定文件。无需新增参数。
7
+ 所有正文来自固定 Git 对象,不读取当前工作区。两侧共享 512 个路径查询、20 个候选及上下文名额、1 MiB 来源正文和 30 秒捕获上限,不因切换版本重新计时。
8
8
 
9
- ## 范围与限制
9
+ ## 版本与证据边界
10
10
 
11
- - 手动文件和自动依赖共享 20 个上下文名额。
12
- - 手动入口正文和自动依赖共享 1 MiB 来源上限;单文件不超过 256 KiB。
13
- - 依赖查询、入口检查及执行前重新采集沿用有限范围和时间上限。
14
- - 自动依赖必须来自 Git 跟踪路径;不会重新纳入排除的变更。
15
- - 文件在预览后发生变化时,必须重新预览,不能沿旧快照发送。
11
+ - oldContextPaths 仍只批准旧侧,不自动增加新侧正文权限。
12
+ - 删除文件复用原批准变更的旧侧身份,不重复作为上下文加入。
13
+ - 排除、阻断或无法确定的依赖不会被恢复为可读证据。
14
+ - 自动取得的普通历史上下文沿用原有两侧正文规则;各版本的依赖分别解析,不用工作区猜测。
16
15
 
17
- 这是保守的文本导航,不是完整符号解析。Git 历史和 SVN 的链式上下文尚未完成,动态调用和实例绑定仍可能无法解析。
16
+ SVN 仍仅支持直接依赖。此功能是保守文本导航,不是完整定义绑定或语义调用图。
18
17
 
19
18
  ## 验证
20
19
 
21
- 真实 Git 和受控模型测试覆盖手动入口、启用开关、上下文数量、正文预算、未跟踪依赖、排除变更和过期拒绝。正式发布还须通过全量回归、独立安装及四组 CI。测试证明流程边界,不证明模型检出率。
20
+ 真实 Git 测试覆盖三层深度、脏工作区隔离、旧侧身份、上下文名额、循环、取消及共享上限。完整回归、宿主检查、独立安装与四组 CI 是正式发布条件,不代表真实模型效果已达标。
22
21
 
23
- 使用方法见 [README](README.md),发布步骤见 [开发指南](DEVELOPMENT.md)。
22
+ 使用方法见 [README](README.md),维护流程见 [开发指南](DEVELOPMENT.md)。
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/SVN working review or Git fixed commit/range review. SVN uses offline normal nodes, rejects unsafe ancestor states and special/keyword properties. 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 tracked literal import/include context shown in preview. Working Git follows changed and explicitly selected context sources at most 3 dependency layers under shared 512 exact probes, 20 candidates/context slots, 1MiB source and 30s verification budget. SVN and Git history remain direct navigation. Not semantic binding or complete definitions.' }, 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/SVN working review or Git fixed commit/range review. SVN uses offline normal nodes, rejects unsafe ancestor states and special/keyword properties. 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 tracked literal import/include context shown in preview. Working Git follows changed and explicitly selected context sources at most 3 dependency layers under shared 512 exact probes, 20 candidates/context slots, 1MiB source and 30s verification budget. Git history follows at most 3 baseline/target dependency layers with shared limits, object reads only; SVN remains direct navigation. Not semantic binding or complete definitions.' }, 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.40.0",
3
+ "version": "0.41.0",
4
4
  "private": false,
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
@@ -1,7 +1,7 @@
1
1
  import {captureHistoryCallers} from './history-caller-capture.mjs';
2
2
  import { isSecretPath, assertNonSecretPath } from './secret-path.mjs';
3
3
  import {captureProjectRules} from './project-rules.mjs';
4
- import {contextCandidates} from './context-candidates.mjs';
4
+ import {captureHistoryDependencyChain} from './history-dependency-chain.mjs';
5
5
  import {realpath} from 'node:fs/promises';
6
6
  import {checked} from './process.mjs';
7
7
  import {hash,decode,relativePath} from './content.mjs';
@@ -52,7 +52,7 @@ export async function captureGitHistory(cwd, options={}) {
52
52
  }
53
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));}});
54
54
  let autoContext;
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
+ if(options.autoContext===true)autoContext=await captureHistoryDependencyChain(root,files,{base,target},context,options);
56
56
  let callerDiscovery;
57
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
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)};
@@ -0,0 +1,50 @@
1
+ import {contextCandidates} from './context-candidates.mjs';
2
+ import {checked} from './process.mjs';
3
+ import {decode,hash,relativePath} from './content.mjs';
4
+ import {assertNonSecretPath} from './secret-path.mjs';
5
+ // Immutable object reads only. Both version sides share every navigation budget.
6
+ export async function captureHistoryDependencyChain(root,files,history,context,options={}){
7
+ const signal=AbortSignal.any([AbortSignal.timeout(30000),...(options.signal?[options.signal]:[])]);
8
+ const bounded={...options,cwd:root,signal},probes=new Set(),visited={old:new Set(files.map(f=>f.path)),new:new Set(files.map(f=>f.path))};
9
+ const maps={old:new Map(),new:new Map()},candidates=new Map(),reserved=new Set([...(options.rulePaths??[]),...(options.oldContextPaths??[]),...(options.contextPaths??[])]);let bytes=0,truncated=false;
10
+ const regular=e=>e?.type==='blob'&&['100644','100755'].includes(e.mode);
11
+ const git=(args,maxBytes=1024*1024)=>checked('git',['--no-optional-locks','--literal-pathspecs',...args],{...bounded,maxBytes});
12
+ const lookup=async(side,paths)=>{const revision=side==='old'?history.base:history.target;if(!revision)return;for(const name of paths){relativePath(name);if(maps[side].has(name))continue;
13
+ const records=new TextDecoder('utf-8',{fatal:true}).decode(await git(['ls-tree','-z','--full-tree',revision,'--',name])).split(String.fromCharCode(0)).filter(Boolean);let entry=null;
14
+ for(const record of records){const m=/^(\d{6}) (blob|commit|tree) ([a-f0-9]+)\t([\s\S]+)$/.exec(record);if(!m||m[4]!==name)throw new Error('Invalid historical path lookup');entry={mode:m[1],type:m[2],oid:m[3]};}maps[side].set(name,entry);
15
+ }};
16
+ const read=async(side,name)=>{assertNonSecretPath(name);const entry=maps[side].get(name);if(!regular(entry))throw new Error('Historical dependency not a regular blob');const value=decode(await git(['cat-file','blob',entry.oid],Math.min(options.maxFileBytes??256*1024,256*1024)));bytes+=value.bytes;if(bytes>1024*1024)throw new Error('Historical dependency source byte limit exceeded');return value;};
17
+ let frontier={old:files.map(f=>({...f,right:f.left,rightExists:!!f.left})),new:[...files]};
18
+ for(const item of context){for(const side of ['old','new']){const text=side==='old'?item.oldText:item.text;if(text===undefined)continue;visited[side].add(item.path);frontier[side].push({path:item.path,eligibility:'reviewable',right:{text}});bytes+=Buffer.byteLength(text);}}
19
+ if(bytes>1024*1024)throw new Error('Historical dependency source byte limit exceeded');
20
+ for(let depth=1;depth<=3;depth++){
21
+ const next={old:[],new:[]},scheduled={old:new Set(),new:new Set()};let any=false;
22
+ for(const side of ['new','old']){if(!frontier[side].length||(side==='old'&&!history.base))continue;any=true;
23
+ const onProbe=paths=>{for(const p of paths)probes.add(p);if(probes.size>512)throw new Error('Historical context lookup limit exceeded');};
24
+ for(let i=0;i<frontier[side].length;i+=200)contextCandidates(frontier[side].slice(i,i+200),[],{onProbe});
25
+ await lookup(side,[...probes]);const paths=[...maps[side]].filter(([,e])=>e).map(([p])=>p),plans=[];
26
+ for(let i=0;i<frontier[side].length;i+=200)plans.push(contextCandidates(frontier[side].slice(i,i+200),paths,{includeChanged:true}));
27
+ for(const plan of plans){truncated ||= plan.truncated;for(const entry of plan.candidates){
28
+ const changed=files.find(f=>f.path===entry.path);
29
+ if(changed){if(side!=='old'||changed.rawStatus!=='D')continue;}
30
+ if(reserved.has(entry.path))continue;
31
+ if(!changed&&visited[side].has(entry.path)){const existing=candidates.get(entry.path);if(existing&&!existing.sourceSides.includes(side))existing.sourceSides.push(side);continue;}visited[side].add(entry.path);
32
+ let c=candidates.get(entry.path);if(!c){if(candidates.size>=20)throw new Error('Historical dependency candidate limit exceeded');c={...entry,depth,sourceSides:[]};candidates.set(entry.path,c);}if(!c.sourceSides.includes(side))c.sourceSides.push(side);
33
+ try{
34
+ if(changed){if(changed.eligibility!=='reviewable'||!changed.left)throw new Error('Old dependency conflicts with excluded blocked or absent old change');c.status='reused-changed-old';continue;}
35
+ let item=context.find(v=>v.path===entry.path);if(!item&&context.length>=Math.min(options.maxContextFiles??20,20))throw new Error('Context file limit exceeded');
36
+ await lookup('new',[entry.path]);await lookup('old',[entry.path]);
37
+ // Preserve direct-context compatibility: regular target and baseline bodies are approved together.
38
+ if(!item){item={path:entry.path};if(regular(maps.new.get(entry.path))){const value=await read('new',entry.path);Object.assign(item,value,{revision:history.target});}
39
+ if(regular(maps.old.get(entry.path))){const value=await read('old',entry.path);Object.assign(item,{oldText:value.text,oldHash:hash(value.text),oldBlobOid:maps.old.get(entry.path).oid,oldRevision:history.base});}
40
+ if(item.text===undefined){if(item.oldText===undefined)throw new Error('Historical dependency not a regular blob');item.oldOnly=true;}context.push(item);
41
+ }
42
+ c.status='captured';for(const s of ['old','new']){const text=s==='old'?item.oldText:item.text;if(text!==undefined&&!scheduled[s].has(item.path)&&(!visited[s].has(item.path)||s===side)){visited[s].add(item.path);scheduled[s].add(item.path);next[s].push({path:item.path,eligibility:'reviewable',right:{text}});}}
43
+ }catch(error){signal.throwIfAborted();if(error.message==='Historical dependency source byte limit exceeded')throw error;c.status='blocked';c.reason=error.message;}
44
+ }}
45
+ }
46
+ frontier=next;if(!any)break;if(depth===3&&(next.old.length||next.new.length))truncated=true;
47
+ }
48
+ signal.throwIfAborted();const ordered=[...candidates.values()].sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);
49
+ return {candidates:ordered,capturedPaths:ordered.filter(c=>c.status==='captured').map(c=>c.path),truncated,maxDepth:3,sourceBytes:bytes,probeCount:probes.size,base:history.base,target:history.target,notice:'Bounded baseline/target literal dependency chains, shared 3 layers, 512 paths, 20 candidates/context slots, 1MiB source and 30s. No worktree reads, semantic binding or complete caller coverage.'};
50
+ }