@feng0531/dsh-code-review 0.45.1 → 0.47.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,17 @@
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.47.0 — 命名空间成员调用导航
6
+
7
+ 批准的 JS/TS 命名空间导入可把独立成员调用指向唯一命名声明;历史逐侧隔离。写入、遮蔽、逃逸、间接调用与歧义拒绝提示;不是实例推导或完整调用图。不新增读取,超预算省略可选线索。
8
+
9
+ ## 0.46.0 — 历史调用方的同版本导航
10
+
11
+ Git 历史调用方可分别对应 baseline 与 target 的唯一声明。批准正文、侧与版本必须对应,缺侧不回退;支持删除目标的旧侧导航,新增文件不产生旧侧声明线索。仍为保守语法导航,不新增读取或证明完整调用覆盖。416 项完整回归、实际宿主、13 项独立安装专项与四组 CI 通过;npm、Release 与原产物 97,482 字节一致,双哈希和清单通过。
12
+
5
13
  ## 0.45.1 — 调用方绑定的歧义与历史隔离
6
14
 
7
- 逐条导入检查唯一解析,防止另一条明确导入掩盖歧义;历史捕获和报告不附带当前侧声明绑定。仅为批准正文中的 JS/TS 命名导入和独立调用位置线索,不是完整语义绑定。替代存在这两项边界错误的 v0.45.0。
15
+ 逐条导入检查唯一解析,防止另一条明确导入掩盖歧义;历史捕获和报告不附带当前侧声明绑定。仅为批准正文中的 JS/TS 命名导入和独立调用位置线索,不是完整语义绑定。替代存在这两项边界错误的 v0.45.0。413 项完整回归、实际宿主、10 项独立安装专项与四组 CI 通过;npm、Release 和原包 96,700 字节一致,双哈希与清单通过。
8
16
 
9
17
  ## 0.45.0 — 必要调用者绑定(由 0.45.1 替代)
10
18
 
@@ -23,18 +23,20 @@
23
23
 
24
24
  | 优先级 | 未完成项 | 验收重点 |
25
25
  |---|---|---|
26
- | 1 | 必要调用方、实例与成员级上下文 | 命名与默认定义位置已交付;调用方绑定修正版待完整复验,实例与成员级仍待验收 |
26
+ | 1 | 必要调用方、实例与成员级上下文 | 命名与默认定义位置、保守调用方线索已交付;历史同版本调用方线索已交付;实例与成员级仍待验收 |
27
27
  | 2 | 剩余历史来源与旧侧组合 | Git 历史与 SVN 三层链已交付;其他旧侧来源仍待验收 |
28
28
  | 3 | 其他旧侧独有来源 | 来源、版本、权限与删除场景保持一致 |
29
29
  | 4 | 剩余语言与路径专用指导 | 只提示具体缺陷和反证;不照搬风格禁令 |
30
30
  | 5 | 剩余预算组合与端到端矩阵 | 分组、窗口、检索、多次审查的终态与覆盖一致 |
31
31
  | 后置 | 真实模型质量评测 | 保存真实输出,人工裁决,分开统计失败和未完成 |
32
32
 
33
- Git 工作区、Git 历史两侧与 SVN 当前侧支持安全的三层字面依赖链;手动批准上下文可以作为入口。命名与默认导入支持批准正文中的唯一声明位置。这些能力不是完整语义绑定。调用方语法绑定的 v0.45.0 存在逐条导入歧义与历史发现元数据混侧错误,不计作安全对齐;v0.45.1 已修正,待完整复验与正式交付。
33
+ Git 工作区、Git 历史两侧与 SVN 当前侧支持安全的三层字面依赖链;手动批准上下文可以作为入口。命名与默认导入支持批准正文中的唯一声明位置。这些能力不是完整语义绑定。调用方语法绑定的 v0.45.0 存在逐条导入歧义与历史发现元数据混侧错误,不计作安全对齐;v0.45.1 已修正并正式交付;历史调用方的同版本语法导航已随 v0.46.0 正式交付;缺侧不用另一侧替代,可选线索超预算时记录省略而不阻断源码审查。
34
34
 
35
35
  ## 当前验证结论
36
36
 
37
- - 最近完整回归:407 项通过。
37
+ - 最近完整回归:416 项通过。
38
+ - v0.46.0 已正式交付历史同版本调用方导航与可选线索预算回退。416 项回归、实际宿主、13 项独立安装专项和四组 CI 通过;npm、Release 与原包 97,482 字节一致,SHA-256、SHA-512 与清单核验通过。
39
+ - v0.45.1 已正式交付保守的调用方语法线索及两项边界修正。413 项回归、实际宿主、10 项独立安装专项和四组 CI 通过;npm、Release 与原包的 96,700 字节一致,SHA-256、SHA-512 与清单核验通过。v0.45.0 已发布但存在逐条导入歧义与历史元数据混侧错误,由 0.45.1 替代,不计作安全对齐。
38
40
  - 实际 DSH 工具运行时检查通过;模型响应受控,不是质量成绩。
39
41
  - v0.39.0 独立安装包的真实 Git、服务与预算测试:5 项通过。
40
42
  - v0.39.1 已正式交付,包含 Git 工作区三层依赖能力。四组 CI 通过;npm、Release 与保存原包的 91,502 字节完全一致,SHA-256、SHA-512 和校验清单通过。v0.39.0 不单独计作已核验交付。
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 可沿安全的当前工作副本节点取得最多三层依赖,不读取 SVN 历史。已批准正文中的唯一命名声明和唯一 default 导出会标出位置;同名、重导出或跨侧内容不猜测。
70
+ 自动上下文和引用方发现是保守的文本导航,不是编译器分析。动态导入、宏、重载、继承和实例方法可能无法解析;找到引用不等于证明函数实际被调用。历史调用方的命名导入线索逐侧对应相同版本声明,标明 side/revision;缺少该版本的批准正文不回退另一侧。Git 历史可沿修改前后两侧分别捕获最多三层依赖;SVN 可沿安全的当前工作副本节点取得最多三层依赖,不读取 SVN 历史。已批准正文中的唯一命名声明和唯一 default 导出会标出位置;同名、重导出或跨侧内容不猜测。命名空间导入后的独立成员调用可导航至唯一命名声明;写入、逃逸或间接调用不推定绑定,不是实例类型分析。
71
71
 
72
72
  ## 如何读报告
73
73
 
package/RELEASE-NOTES.md CHANGED
@@ -1,21 +1,18 @@
1
- # v0.45.1 发布说明
1
+ # v0.47.0 发布说明
2
2
 
3
- ## 新增:必要调用者绑定
3
+ ## 命名空间成员调用导航
4
4
 
5
- 启用 callerScopePaths 后,已捕获的调用方文件会附带语法绑定:导入行、本地别名、调用行,以及变更文件中唯一声明的所在行。这回答“谁调用了你改的函数”,而不只是“谁提到了这个文件”。
5
+ JS/TS 的 import * as api 后,独立 api.run() 调用可以指向已批准目标正文中的唯一命名声明。线索标出导入行、命名空间别名、成员名、调用行与声明行,进入上下文关系和批准调用方导航。历史两侧独立解析,不混用声明。
6
6
 
7
- 审查输入中的绑定只在调用方正文与变更文件正文都已批准时提供,不新增文件读取。当前仅支持保守的 JS/TS 命名导入与独立调用语法。
7
+ ## 保持的限制
8
8
 
9
- 每一条导入必须独立唯一解析;同文件另一条明确导入不能替歧义导入提供授权。历史捕获、预览、报告与审查输入均不产生这类绑定,避免新旧声明混用。v0.45.1 替代存在这两项边界错误的 v0.45.0。
9
+ - 不是实例类型分析或完整调用图,不推定继承、动态分派或执行可达。
10
+ - 每条导入须唯一解析;命名空间写入、遮蔽、逃逸、间接或复合调用、嵌套参数调用、反斜杠转义、JSX 类文本及注释模板不生成线索。
11
+ - 每条导入最多 20 条;更多调用拒绝提示,不把截断当作完整覆盖。
12
+ - 不新增读取或放宽预算;可选成员线索超输入预算时省略并记录,批准正文保留。
10
13
 
11
- ## 限制
14
+ ## 验证范围
12
15
 
13
- - 调用方正文未获批(例如超出上下文名额)或变更声明有歧义时不给出绑定。
14
- - 历史审查不产生这类调用方绑定,避免把当前侧调用行当作旧版本证据。
15
- - 绑定不证明执行可达、动态分派、重载选择或完整调用覆盖,也不代表全部调用方。
16
+ 覆盖成员调用、歧义路径、名称变更与逃逸反例、历史缺侧及模型输入。完整回归、宿主检查、独立安装和四矩阵 CI 是正式发布条件,受控模型响应不是效果成绩。
16
17
 
17
- ## 验证
18
-
19
- 测试覆盖真实 Git 调用方绑定、未批准调用方拒绝、歧义拒绝、审查输入投递与历史排除。全量回归、实际宿主、独立安装和四组 CI 是正式发布条件,不是模型效果成绩。
20
-
21
- 使用方法见 [README](README.md),维护步骤见 [开发指南](DEVELOPMENT.md)。
18
+ 使用方法见 [README](README.md)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feng0531/dsh-code-review",
3
- "version": "0.45.1",
3
+ "version": "0.47.0",
4
4
  "private": false,
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
@@ -0,0 +1,12 @@
1
+ // Pure annotations over approved snapshot bodies. No capture, reads or coverage.
2
+ export function approvedCallerBindings(input, file) {
3
+ const contexts=input.context??[],candidates=input.callerDiscovery?.candidates??[];
4
+ return candidates.filter(candidate=>{
5
+ if(candidate.targetPath!==file.path||!candidate.bindings?.length)return false;
6
+ if(!input.history)return !candidate.side&&contexts.some(c=>c.path===candidate.fromPath&&typeof c.text==='string');
7
+ const side=candidate.side,revision=side==='old'?input.history.base:side==='new'?input.history.target:undefined;
8
+ if(!revision||candidate.revision!==revision)return false;
9
+ if(side==='old'?(file.rawStatus==='A'||typeof file.left?.text!=='string'):(file.rightExists===false||typeof file.right?.text!=='string'))return false;
10
+ return contexts.some(c=>c.path===candidate.fromPath&&(side==='old'?typeof c.oldText==='string'&&c.oldRevision===revision:typeof c.text==='string'&&c.revision===revision));
11
+ }).map(({fromPath,bindings,side,revision})=>({fromPath,bindings,...(input.history?{side,revision,targetRevision:revision,contextSide:side==='old'?'context-old':'context'}:{})}));
12
+ }
@@ -1,5 +1,6 @@
1
1
  import { contextCandidates } from './context-candidates.mjs';
2
2
  import { directImportBindings } from './import-bindings.mjs';
3
+ import { namespaceImportBindings } from './namespace-bindings.mjs';
3
4
  import { importedCallSites, indexStandaloneCalls } from './import-call-sites.mjs';
4
5
  import { relativePath } from './content.mjs';
5
6
  import { isSecretPath } from './secret-path.mjs';
@@ -17,7 +18,7 @@ function callerBindings(fromPath, targetPath, scannedText, targetText, resolutio
17
18
  const candidates = path.posix.extname(target) ? [target] : [target, ...importExtensions.map(ext => target + ext), ...importExtensions.map(ext => target + '/index' + ext)];
18
19
  const matches = candidates.filter(candidate => resolutionPaths.has(candidate));
19
20
  if (matches.length !== 1 || matches[0] !== targetPath) continue;
20
- result.push(...directImportBindings(lines, i + 1, importedCallSites(lines, i + 1, callIndex), targetText));
21
+ result.push(...directImportBindings(lines, i + 1, importedCallSites(lines, i + 1, callIndex), targetText), ...namespaceImportBindings(lines, i + 1, targetText));
21
22
  }
22
23
  return result.slice(0, 20);
23
24
  }
@@ -9,7 +9,7 @@ export async function captureHistoryCallers(root,files,history,scopes,options={}
9
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
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
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,{includeBindings:false});indexes.push({side,commit,indexFingerprint:index.fingerprint,probeFingerprint:probes.fingerprint});for(const c of plan.candidates)candidates.push({...c,side,revision:commit});}
12
+ const targets=side==='old'?files.map(f=>({...f,right:f.left,rightExists:f.rawStatus!=='A'&&typeof f.left?.text==='string'})):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
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
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
15
  }
@@ -1,4 +1,5 @@
1
1
  import path from 'node:path';
2
+ import { namespaceImportBindings } from './namespace-bindings.mjs';
2
3
  import { directImportBindings, namedImportDefinitions, defaultImportDefinition } from './import-bindings.mjs';
3
4
  import { importedCallSites, indexStandaloneCalls } from './import-call-sites.mjs';
4
5
 
@@ -38,7 +39,7 @@ export function inferImportRelations(files) {
38
39
  if (matches.length !== 1 || matches[0].eligibility !== 'reviewable' || matches[0].id === file.id) continue;
39
40
  const callSites = importedCallSites(lines, i + 1, callIndex);
40
41
  const targetText = (side === 'old' ? matches[0].left : matches[0].right)?.text;
41
- const bindingHints = directImportBindings(lines, i + 1, callSites, targetText);
42
+ const bindingHints = [...directImportBindings(lines, i + 1, callSites, targetText), ...namespaceImportBindings(lines, i + 1, targetText)].slice(0,20);
42
43
  const definitionHints = [...namedImportDefinitions(lines[i], targetText), ...defaultImportDefinition(lines[i], targetText)];
43
44
  edges.push({ from: file.id, to: matches[0].id, reason: 'relative-import:' + side + ':L' + (i + 1), side, line: i + 1, specifier: match[2], callSites, bindingHints, definitionHints });
44
45
  if (callSites.length && edges.length < 2000) edges.push({ from: file.id, to: matches[0].id, reason: 'imported-call-syntax:' + side + ':L' + callSites.map(c => c.line).join(','), side, callSites });
@@ -17,7 +17,15 @@ export function prepareInteractionInput(snapshot, group, measure) {
17
17
  if (group.fileIds.filter(id => { const file = snapshot.files.find(f => f.id === id); return file.left.text !== file.right.text; }).length < 2) return { status: 'not-applicable', fileIds: [...group.fileIds] };
18
18
  if (files.some(f => f.unavailable)) return { status: 'blocked', reason: 'exact-edit-analysis-unavailable', fileIds: [...group.fileIds] };
19
19
  const contextRelations = group.fileIds.flatMap(id=>inferContextRelations(snapshot,snapshot.files.find(f=>f.id===id)).map(relation=>({primaryFileId:id,...relation})));
20
- const payload = JSON.stringify({ ...(snapshot.history ? {contextSideNotice:CONTEXT_SIDE_NOTICE} : {}), ...(snapshot.businessRequirement!==undefined?{businessRequirement:snapshot.businessRequirement,requirementNotice:REQUIREMENT_NOTICE}:{}), contextRelations, contextRelationNotice:'Approved-context import/include and call binding hints only; not a call graph, complete caller coverage, semantic binding or causal evidence. Historical contextSide/contextVersion must not be interchanged.', snapshotId: snapshot.id, sourceMode: 'file-interaction', groupId: group.id, files, context: snapshot.context ?? [], rules: snapshot.rules ?? [], notice: 'Only combined defects across at least two listed files; directories are navigation not evidence. Read referenced edit source sides from approved catalog in this generation. Return ordinary findings with fileId of one listed primary and exact original line anchor; attribution.editIds references only that primary. Add interaction:{editRefs:[{fileId,editId}]} including all involved edits across at least two files. This sourceMode overrides single-primary input instructions. Retain original primary anchor and file-qualified edit references. Unsupported conclusions become limitations; no repeated single-file issues. Relations and read coverage are not causal proof.' });
21
- const budget = measure(payload);
22
- return { status: budget.fits ? 'ready' : 'blocked', reason: budget.fits ? undefined : 'input-budget', fileIds: [...group.fileIds], payload, budget };
20
+ let payload = JSON.stringify({ ...(snapshot.history ? {contextSideNotice:CONTEXT_SIDE_NOTICE} : {}), ...(snapshot.businessRequirement!==undefined?{businessRequirement:snapshot.businessRequirement,requirementNotice:REQUIREMENT_NOTICE}:{}), contextRelations, contextRelationNotice:'Approved-context import/include and call binding hints only; not a call graph, complete caller coverage, semantic binding or causal evidence. Historical contextSide/contextVersion must not be interchanged.', snapshotId: snapshot.id, sourceMode: 'file-interaction', groupId: group.id, files, context: snapshot.context ?? [], rules: snapshot.rules ?? [], notice: 'Only combined defects across at least two listed files; directories are navigation not evidence. Read referenced edit source sides from approved catalog in this generation. Return ordinary findings with fileId of one listed primary and exact original line anchor; attribution.editIds references only that primary. Add interaction:{editRefs:[{fileId,editId}]} including all involved edits across at least two files. This sourceMode overrides single-primary input instructions. Retain original primary anchor and file-qualified edit references. Unsupported conclusions become limitations; no repeated single-file issues. Relations and read coverage are not causal proof.' });
21
+ let budget = measure(payload);
22
+ const metadata={};
23
+ const omitted=contextRelations.reduce((n,e)=>n+(e.bindingHints??[]).filter(h=>h.confidence==='namespace-member-syntax-only').length,0);
24
+ if(!budget.fits&&omitted){
25
+ const envelope=JSON.parse(payload);
26
+ envelope.contextRelations=envelope.contextRelations.map(e=>({...e,bindingHints:(e.bindingHints??[]).filter(h=>h.confidence!=='namespace-member-syntax-only')}));
27
+ payload=JSON.stringify(envelope);budget=measure(payload);
28
+ metadata.namespaceBindingFallback='input-budget';metadata.omittedNamespaceBindingCount=omitted;
29
+ }
30
+ return { status: budget.fits ? 'ready' : 'blocked', reason: budget.fits ? undefined : 'input-budget', fileIds: [...group.fileIds], payload, budget, ...metadata };
23
31
  }
@@ -0,0 +1,26 @@
1
+ import {uniqueApprovedDeclarations} from './import-bindings.mjs';
2
+ // Pure syntax navigation over already approved text; no inferred instance type.
3
+ export function namespaceImportBindings(lines, importLine, targetText) {
4
+ if(!Array.isArray(lines)||lines.length>20000||typeof targetText!=='string')return [];
5
+ const text=lines.join('\n');
6
+ // Reject escape-normalized names and JSX-like lexical regions without a parser.
7
+ if (/[\\<>]/.test(text) || /[\\<>]/.test(targetText)) return [];
8
+ if(Buffer.byteLength(text)>256*1024||text.includes('/*')||text.includes(String.fromCharCode(96))||/\\\r?\n/.test(text))return [];
9
+ const match=/^\s*import\s+\*\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)\s+from\s+(['"])[^'"\\]+\2\s*;?\s*(?:\/\/.*)?$/.exec(lines[importLine-1]);
10
+ if(!match)return [];
11
+ const local=match[1],escaped=local.replaceAll('$','\\$'),token=new RegExp('(^|[^A-Za-z0-9_$])'+escaped+'(?=[^A-Za-z0-9_$]|$)','g');
12
+ const pattern=new RegExp('^\\s*(?:await\\s+)?'+escaped+'\\.([A-Za-z_$][A-Za-z0-9_$]*)\\s*\\([^();"\x27/]*\\)\\s*;?\\s*$');
13
+ const calls=[];
14
+ for(let i=0;i<lines.length;i++){
15
+ if(i+1===importLine)continue;
16
+ const occurrences=[...lines[i].matchAll(token)];if(!occurrences.length)continue;
17
+ const call=pattern.exec(lines[i]);
18
+ // Other occurrences may mutate, shadow, pass, destructure or escape the namespace.
19
+ if(!call||occurrences.length!==1)return [];
20
+ if(calls.length>=20)return [];
21
+ calls.push({member:call[1],callLine:i+1});
22
+ }
23
+ const targets=targetText.split('\n'),result=[];
24
+ for(const call of calls){const declarations=uniqueApprovedDeclarations(targets,call.member);if(declarations.length===1)result.push({imported:call.member,local,member:call.member,importLine,callLine:call.callLine,declarationLine:declarations[0].number,declarationKind:declarations[0].kind,confidence:'namespace-member-syntax-only'});}
25
+ return result;
26
+ }
@@ -1,4 +1,5 @@
1
1
  import { CONTEXT_SIDE_NOTICE } from './context-identity.mjs';
2
+ import { approvedCallerBindings } from './caller-binding-input.mjs';
2
3
  import { REQUIREMENT_NOTICE } from './business-requirement.mjs';
3
4
  import { defectGuidance } from './defect-guidance.mjs';
4
5
  import { cppCallSites } from './cpp-call-sites.mjs';
@@ -26,14 +27,27 @@ export function preparePrimaryInput(input, file, { instructions, scope, grouping
26
27
  }
27
28
  const csharpHints = inferCsharpContext(input, file);
28
29
  if (csharpHints.length) { base.csharpContextHints = csharpHints; base.csharpContextNotice = '仅批准context的C#静态类型/方法语法位置;历史输入按contextSide/contextVersion区分正文,无旧正文不回退目标侧。不证明语义绑定或执行可达。歧义不推定,线索不增加审查覆盖。'; }
29
- if (!input.history && input.callerDiscovery) {
30
- const approved = new Set((input.context ?? []).filter(c => typeof c.text === 'string').map(c => c.path));
31
- const callerBindings = input.callerDiscovery.candidates.filter(c => c.targetPath === file.path && c.bindings?.length && approved.has(c.fromPath)).map(({ fromPath, bindings }) => ({ fromPath, bindings }));
32
- if (callerBindings.length) { base.callerBindings = callerBindings; base.callerBindingNotice = '调用方文件的导入行、调用行与目标声明行仅为语法位置线索,来源限于已批准上下文;不证明执行可达、动态分派、重载选择或完整调用覆盖,也不代表全部调用方。'; }
30
+ if (input.callerDiscovery) {
31
+ const callerBindings = approvedCallerBindings(input, file);
32
+ if (callerBindings.length) { base.callerBindings = callerBindings; base.callerBindingNotice = '调用方文件的导入行、调用行与目标声明行仅为语法位置线索,来源限于已批准上下文;历史side/revision要求调用方与目标声明同版本,缺侧不回退。仅导航,窗口中的原文须按相应版本catalog读取;不证明执行可达、动态分派、重载选择或完整调用覆盖,也不代表全部调用方。'; }
33
33
  }
34
34
  let payload = JSON.stringify(base);
35
35
  const measure = value => initialInputBudget({ instructions, input: value }, scope, maxInputBytes);
36
36
  const metadata = {};
37
+ // Newly added member navigation is optional, like caller annotations.
38
+ if (base.contextRelations?.some(e=>e.bindingHints?.some(h=>h.confidence==='namespace-member-syntax-only')) && !measure(payload).fits) {
39
+ metadata.namespaceBindingFallback = 'input-budget';
40
+ metadata.omittedNamespaceBindingCount = base.contextRelations.reduce((n,e)=>n+(e.bindingHints??[]).filter(h=>h.confidence==='namespace-member-syntax-only').length,0);
41
+ base.contextRelations = base.contextRelations.map(e=>({...e,bindingHints:(e.bindingHints??[]).filter(h=>h.confidence!=='namespace-member-syntax-only')}));
42
+ payload = JSON.stringify(base);
43
+ }
44
+ // Optional navigation must not block source review or force extra windows.
45
+ if (base.callerBindings?.length && !measure(payload).fits) {
46
+ metadata.callerBindingFallback = 'input-budget';
47
+ metadata.omittedCallerBindingCount = base.callerBindings.reduce((count,c)=>count+c.bindings.length,0);
48
+ delete base.callerBindings; delete base.callerBindingNotice;
49
+ payload = JSON.stringify(base);
50
+ }
37
51
  let batches, synthesis;
38
52
  const group = grouping?.groups.find(g => g.fileIds.includes(file.id));
39
53
  if (group) {
@@ -44,7 +58,14 @@ export function preparePrimaryInput(input, file, { instructions, scope, grouping
44
58
  const bindingRelations = inferImportRelations(input.files).filter(edge => (edge.bindingHints?.length || edge.definitionHints?.length) && (edge.from === file.id || edge.to === file.id) && group.fileIds.includes(edge.from) && group.fileIds.includes(edge.to)).map(({ from, to, side, bindingHints, definitionHints }) => ({ from, to, side, bindingHints, ...(definitionHints?.length ? { definitionHints } : {}) }));
45
59
  const grouped = JSON.stringify({ ...base, relatedFiles, relations, bindingRelations, relationNotice: '命名和相对导入关系仅为源码线索,可能被同名局部变量遮蔽,不证明绑定、调用可达或因果;只报告主file的问题,不为relatedFiles重复生成发现。' });
46
60
  if (measure(grouped).fits) { payload = grouped; metadata.relatedFileIds = relatedFiles.map(f => f.id); }
47
- else metadata.groupFallback = 'input-budget';
61
+ else {
62
+ // Optional member hints must not cost the grouped related sources either.
63
+ const groupedOmitted = bindingRelations.reduce((n, e) => n + (e.bindingHints ?? []).filter(h => h.confidence === 'namespace-member-syntax-only').length, 0);
64
+ const trimmedRelations = bindingRelations.map(e => ({ ...e, bindingHints: (e.bindingHints ?? []).filter(h => h.confidence !== 'namespace-member-syntax-only') })).filter(e => e.bindingHints.length || e.definitionHints?.length);
65
+ const trimmed = JSON.stringify({ ...base, relatedFiles, relations, bindingRelations: trimmedRelations, relationNotice: '命名和相对导入关系仅为源码线索,可能被同名局部变量遮蔽,不证明绑定、调用可达或因果;只报告主file的问题,不为relatedFiles重复生成发现。' });
66
+ if (groupedOmitted && measure(trimmed).fits) { payload = trimmed; metadata.relatedFileIds = relatedFiles.map(f => f.id); metadata.namespaceBindingFallback = 'input-budget'; metadata.omittedNamespaceBindingCount = (metadata.omittedNamespaceBindingCount ?? 0) + groupedOmitted; }
67
+ else metadata.groupFallback = 'input-budget';
68
+ }
48
69
  }
49
70
  }
50
71
  if (!measure(payload).fits && scope) {