@feng0531/dsh-code-review 0.82.3 → 0.83.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 +8 -0
- package/README.md +1 -1
- package/RELEASE-NOTES.md +8 -7
- package/index.mjs +11 -12
- package/package.json +1 -1
- package/src/background-review.mjs +84 -11
- package/src/business-groups.mjs +1 -1
- package/src/class-bindings.mjs +45 -12
- package/src/defect-guidance.mjs +158 -16
- package/src/dsh-model.mjs +107 -15
- package/src/git-history.mjs +202 -56
- package/src/git.mjs +64 -34
- package/src/import-bindings.mjs +74 -65
- package/src/job-progress.mjs +21 -0
- package/src/model-contract.mjs +17 -1
- package/src/primary-input.mjs +85 -26
- package/src/project-report.mjs +22 -5
- package/src/project-review-service.mjs +177 -37
- package/src/project-snapshot.mjs +101 -40
- package/src/prompts.mjs +9 -0
- package/src/protocol-guidance.mjs +13 -1
- package/src/recovery-coverage.mjs +25 -1
- package/src/retrieval-loop.mjs +66 -28
- package/src/review-jobs.mjs +6 -4
- package/src/review.mjs +36 -33
- package/src/risk-plan.mjs +1 -1
- package/src/service.mjs +136 -5
- package/src/snapshot-reader.mjs +18 -5
- package/src/snapshot.mjs +69 -42
- package/src/svn-capture-budget.mjs +22 -5
- package/src/svn-effective-mergeinfo.mjs +59 -37
- package/src/svn-eol-mapping.mjs +22 -10
- package/src/svn-history-endpoints.mjs +18 -12
- package/src/svn-history-lookup.mjs +34 -26
- package/src/svn-keyword-config.mjs +11 -8
- package/src/svn-keyword-mapping.mjs +67 -36
- package/src/svn-mergeinfo.mjs +34 -22
- package/src/svn-navigation-budget.mjs +15 -4
- package/src/svn-special.mjs +13 -8
- package/src/svn-url.mjs +18 -6
- package/src/svn-working-identity.mjs +22 -8
- package/src/svn-working-scope.mjs +16 -5
- package/src/tool-view.mjs +5 -0
- package/src/verification-loop.mjs +2 -1
package/src/import-bindings.mjs
CHANGED
|
@@ -1,81 +1,90 @@
|
|
|
1
1
|
// Conservative direct exported-function hints, never execution/reachability proof.
|
|
2
|
+
function identifierPattern(name, suffix, flags = '') {
|
|
3
|
+
return new RegExp('(^|[^A-Za-z0-9_$])' + name.replaceAll('$', '\\$') + suffix, flags);
|
|
4
|
+
}
|
|
5
|
+
|
|
2
6
|
export function directImportBindings(lines, importLine, calls, targetText) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
7
|
+
if (typeof targetText !== 'string' || Buffer.byteLength(targetText) > 256 * 1024 || /[\x60]|\/\*/.test(targetText)) return [];
|
|
8
|
+
const match = /^\s*import\s+\{([^}]+)\}\s+from\s+(['"])[^'"]+\2\s*;?\s*(?:\/\/.*)?$/.exec(lines[importLine - 1]);
|
|
9
|
+
if (!match) return [];
|
|
10
|
+
const bindings = match[1].split(',').map(part => /^\s*([A-Za-z_$][A-Za-z0-9_$]*)(?:\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*))?\s*$/.exec(part));
|
|
11
|
+
if (bindings.some(b => !b)) return [];
|
|
12
|
+
const targets = targetText.split(String.fromCharCode(10));
|
|
13
|
+
const result = [];
|
|
14
|
+
for (const binding of bindings) {
|
|
15
|
+
const imported = binding[1], local = binding[2] ?? imported;
|
|
16
|
+
const token = identifierPattern(local, '([^A-Za-z0-9_$]|$)');
|
|
17
|
+
const positions = calls.filter(c => c.local === local);
|
|
18
|
+
if (!positions.length) continue;
|
|
19
|
+
// Arguments may mutate/escape the alias on the SAME indexed call line.
|
|
20
|
+
// Keep a hint only when the sole local token is the call callee.
|
|
21
|
+
const occurrences = identifierPattern(local, '(?=[^A-Za-z0-9_$]|$)', 'g');
|
|
22
|
+
if (positions.some(call => [...lines[call.line - 1].matchAll(occurrences)].length !== 1)) continue;
|
|
23
|
+
// Every other occurrence may be declaration, parameter, mutation or escape.
|
|
24
|
+
if (lines.some((line, i) => i + 1 !== importLine && !positions.some(c => c.line === i + 1) && token.test(line))) continue;
|
|
25
|
+
const declarations = uniqueApprovedDeclarations(targets, imported);
|
|
26
|
+
if (declarations.length !== 1) continue;
|
|
27
|
+
const exportedToken = identifierPattern(imported, '([^A-Za-z0-9_$]|$)');
|
|
28
|
+
if (targets.some((line, i) => i + 1 !== declarations[0].number && exportedToken.test(line))) continue;
|
|
29
|
+
result.push(...positions.map(call => ({ imported, local, importLine, callLine: call.line, declarationLine: declarations[0].number, declarationKind: declarations[0].kind, confidence: 'conservative-syntax-only' })));
|
|
30
|
+
}
|
|
31
|
+
return result.slice(0, 20);
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
// Only one direct declaration inside already approved text. No extra file reads.
|
|
30
35
|
export function uniqueApprovedDeclarations(lines, imported) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
if (!Array.isArray(lines) || lines.length > 20000 || typeof imported !== 'string' || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(imported)) return [];
|
|
37
|
+
const text = lines.join(String.fromCharCode(10));
|
|
38
|
+
if (lines.some(line => typeof line !== 'string') || Buffer.byteLength(text) > 256 * 1024 || text.includes('/*') || text.includes(String.fromCharCode(96)) || text.includes(String.fromCharCode(92, 10)) || text.includes(String.fromCharCode(92, 13, 10))) return [];
|
|
39
|
+
const forms = [
|
|
40
|
+
['function', /^\s*export\s+(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/],
|
|
41
|
+
['const', /^\s*export\s+(?:const|let)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=/],
|
|
42
|
+
['class', /^\s*export\s+class\s+([A-Za-z_$][A-Za-z0-9_$]*)(?=\s|\{)/],
|
|
43
|
+
];
|
|
44
|
+
const found = [];
|
|
45
|
+
for (let i = 0; i < lines.length; i++) {
|
|
46
|
+
for (const [kind, pattern] of forms) {
|
|
47
|
+
if (pattern.exec(lines[i])?.[1] === imported) found.push({ number: i + 1, kind });
|
|
48
|
+
}
|
|
43
49
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return found;
|
|
50
|
+
if (found.length !== 1) return [];
|
|
51
|
+
const occurrences = identifierPattern(imported, '(?=[^A-Za-z0-9_$]|$)', 'g');
|
|
52
|
+
if ([...lines[found[0].number - 1].matchAll(occurrences)].length !== 1) return [];
|
|
53
|
+
const token = identifierPattern(imported, '([^A-Za-z0-9_$]|$)');
|
|
54
|
+
if (lines.some((line, i) => i + 1 !== found[0].number && token.test(line))) return [];
|
|
55
|
+
return found;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
// Named imports only. Missing, duplicate, comment, or reexport names stay unresolved.
|
|
54
59
|
export function namedImportDefinitions(importLine, targetText) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
60
|
+
if (typeof targetText !== 'string' || Buffer.byteLength(targetText) > 256 * 1024 || /[\x60]|\/\*/.test(targetText)) return [];
|
|
61
|
+
const match = /^\s*import\s+(?:[A-Za-z_$][A-Za-z0-9_$]*\s*,\s*)?\{([^}]+)\}\s+from\s+(['"])[^'"]+\2\s*;?\s*(?:\/\/.*)?$/.exec(importLine);
|
|
62
|
+
if (!match) return [];
|
|
63
|
+
const names = match[1].split(',').map(part => /^\s*([A-Za-z_$][A-Za-z0-9_$]*)(?:\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*))?\s*$/.exec(part));
|
|
64
|
+
if (!names.length || names.some(item => !item) || new Set(names.map(item => item[1])).size !== names.length || new Set(names.map(item => item[2] ?? item[1])).size !== names.length) return [];
|
|
65
|
+
const lines = targetText.split(String.fromCharCode(10));
|
|
66
|
+
const result = [];
|
|
67
|
+
for (const name of names) {
|
|
68
|
+
const found = uniqueApprovedDeclarations(lines, name[1]);
|
|
69
|
+
if (found.length === 1) result.push({ imported: name[1], declarationLine: found[0].number, declarationKind: found[0].kind, confidence: 'approved-unique-syntax-only' });
|
|
70
|
+
}
|
|
71
|
+
return result.slice(0, 20);
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
// Unique default export inside already approved text; anonymous defaults keep their line only.
|
|
69
75
|
export function defaultImportDefinition(importLine, targetText) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (declarations.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
if (typeof targetText !== 'string' || Buffer.byteLength(targetText) > 256 * 1024 || /[\x60]|\/\*/.test(targetText)) return [];
|
|
77
|
+
if (targetText.includes(String.fromCharCode(92, 10)) || targetText.includes(String.fromCharCode(92, 13, 10))) return [];
|
|
78
|
+
const match = /^\s*import\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:,\s*\{[^}]*\}\s*)?from\s+(['"])[^'"]+\2\s*;?\s*(?:\/\/.*)?$/.exec(importLine);
|
|
79
|
+
if (!match) return [];
|
|
80
|
+
const lines = targetText.split(String.fromCharCode(10));
|
|
81
|
+
const declarations = [];
|
|
82
|
+
for (let i = 0; i < lines.length; i++) if (/^\s*export\s+default\b/.test(lines[i])) declarations.push(i + 1);
|
|
83
|
+
if (declarations.length !== 1) return [];
|
|
84
|
+
const line = lines[declarations[0] - 1];
|
|
85
|
+
const kind = /^\s*export\s+default\s+(?:function|class)\b/.test(line) ? 'default-declaration'
|
|
86
|
+
: /^\s*export\s+default\s+[^=;]+;\s*(?:\/\/.*)?$/.test(line) ? 'default-expression'
|
|
87
|
+
: undefined;
|
|
88
|
+
if (!kind) return [];
|
|
89
|
+
return [{ imported: 'default', local: match[1], declarationLine: declarations[0], declarationKind: kind, confidence: 'approved-default-syntax-only' }];
|
|
81
90
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { recoveryCoverage } from './recovery-coverage.mjs';
|
|
2
|
+
|
|
3
|
+
const NOTICE = 'Percent measures terminal primary-file processing, not successful review or synthesis completion. Unknown capture denominator is null, never invented.';
|
|
4
|
+
|
|
5
|
+
export function jobProgress(record) {
|
|
6
|
+
const r = recoveryCoverage(record);
|
|
7
|
+
const counts = { completed: 0, excluded: 0, blocked: 0, failed: 0, cancelled: 0, pending: 0 };
|
|
8
|
+
for (const f of r.files) counts[f.status] = (counts[f.status] ?? 0) + 1;
|
|
9
|
+
const terminal = !['queued', 'running'].includes(record.status);
|
|
10
|
+
const processed = r.files.filter(f => !['pending'].includes(f.status)).length;
|
|
11
|
+
return {
|
|
12
|
+
stage: record.stage,
|
|
13
|
+
terminal,
|
|
14
|
+
elapsedMs: Date.now() - Date.parse(record.createdAt),
|
|
15
|
+
total: r.manifestSaved ? r.files.length : null,
|
|
16
|
+
counts,
|
|
17
|
+
processed,
|
|
18
|
+
percent: r.manifestSaved && r.files.length ? Math.floor(100 * processed / r.files.length) : null,
|
|
19
|
+
notice: NOTICE,
|
|
20
|
+
};
|
|
21
|
+
}
|
package/src/model-contract.mjs
CHANGED
|
@@ -1,2 +1,18 @@
|
|
|
1
1
|
// Describe only transport shape; never invent evidence or fill missing fields.
|
|
2
|
-
|
|
2
|
+
const VERDICT_CONTRACT = '本阶段必须返回 {"verdicts":[{"candidateId":"c1","verdict":"uncertain","reason":"证据不足原因","evidence":[]}]},candidateId必须来自本次candidates;supported/refuted证据规则仍按原指令。不是findings响应。';
|
|
3
|
+
const GROUPS_CONTRACT = '本阶段返回groups数组,每项含fileIds和reason,文件身份必须来自输入,不是findings响应。';
|
|
4
|
+
const RISK_CONTRACT = '本阶段返回 {"risks":[]} 或原指令要求的风险条目,不是findings。';
|
|
5
|
+
const INTERACTION_CONTRACT = '本阶段返回 {"findings":[],"limitations":[]};发现必须包含原指令全部字段和跨文件interaction.editRefs。无足够跨文件证据时说明limitations,不编造。';
|
|
6
|
+
const FINDINGS_CONTRACT = '本阶段最终返回 {"findings":[],"limitations":[]}。每项finding必须含fileId、severity(critical/high/medium/low)、title、evidence、trigger、impact、suggestion和anchor;fileId原样复制file.id,不用文件名替代。anchor.line用side/start/end/snippet且精确匹配原行;需要更多证据可按检索协议先返回requests。';
|
|
7
|
+
|
|
8
|
+
export function modelContract(input, instructions = '') {
|
|
9
|
+
let p;
|
|
10
|
+
try { p = JSON.parse(input); } catch { return ''; }
|
|
11
|
+
if (!p || typeof p !== 'object' || Array.isArray(p)) return '';
|
|
12
|
+
if (Array.isArray(p.candidates)) return VERDICT_CONTRACT;
|
|
13
|
+
if (/返回\{groups:/.test(instructions)) return GROUPS_CONTRACT;
|
|
14
|
+
if (/仅制定审查计划/.test(instructions)) return RISK_CONTRACT;
|
|
15
|
+
if (p.sourceMode === 'file-interaction') return INTERACTION_CONTRACT;
|
|
16
|
+
if (p.file) return FINDINGS_CONTRACT;
|
|
17
|
+
return '';
|
|
18
|
+
}
|
package/src/primary-input.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {sourceSegments} from './source-segments.mjs';
|
|
2
|
-
import {changeRegion} from './change-region.mjs';
|
|
1
|
+
import { sourceSegments } from './source-segments.mjs';
|
|
2
|
+
import { changeRegion } from './change-region.mjs';
|
|
3
3
|
import { CONTEXT_SIDE_NOTICE } from './context-identity.mjs';
|
|
4
4
|
import { approvedCallerBindings } from './caller-binding-input.mjs';
|
|
5
5
|
import { REQUIREMENT_NOTICE } from './business-requirement.mjs';
|
|
@@ -14,44 +14,85 @@ import { changeWindows } from './change-windows.mjs';
|
|
|
14
14
|
import { changeMap } from './change-map.mjs';
|
|
15
15
|
import { initialInputBudget } from './input-budget.mjs';
|
|
16
16
|
|
|
17
|
+
const CPP_FILE = /\.(?:c|cc|cpp|cxx|h|hh|hpp|hxx)$/;
|
|
18
|
+
const RELATION_NOTICE = '命名和相对导入关系仅为源码线索,可能被同名局部变量遮蔽,不证明绑定、调用可达或因果;只报告主file的问题,不为relatedFiles重复生成发现。';
|
|
19
|
+
const optional = h => h.confidence === 'namespace-member-syntax-only' || h.confidence === 'instance-method-syntax-only';
|
|
20
|
+
|
|
17
21
|
// Pure preparation over already captured data. No model calls or new reads.
|
|
18
22
|
export function preparePrimaryInput(input, file, { instructions, scope, grouping, maxInputBytes }) {
|
|
19
23
|
let changes = changeMap(file.left.text, file.right.text);
|
|
20
|
-
if(changes.status==='limited')
|
|
21
|
-
|
|
24
|
+
if (changes.status === 'limited') {
|
|
25
|
+
const region = changeRegion(file.left.text, file.right.text);
|
|
26
|
+
if (region.status === 'changed') {
|
|
27
|
+
changes = {
|
|
28
|
+
status: 'changed', precision: 'envelope',
|
|
29
|
+
reason: 'Exact diff unavailable; broad region includes potentially unchanged lines, not exact edits or causal proof.',
|
|
30
|
+
edits: [{ id: 'envelope1', old: region.old, new: region.new }],
|
|
31
|
+
hunks: [{ id: 'envelope-h1', old: region.old, new: region.new, editIds: ['envelope1'] }],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const base = {
|
|
37
|
+
...(input.projectScope ? { projectScope: input.projectScope, reviewNotice: 'Full current-file review; empty left is a sentinel, not a historical baseline. changes are transport coordinates only, do not infer introduced regression.' } : {}),
|
|
38
|
+
...(input.history ? { contextSideNotice: CONTEXT_SIDE_NOTICE } : {}),
|
|
39
|
+
...(input.businessRequirement !== undefined ? { businessRequirement: input.businessRequirement, requirementNotice: REQUIREMENT_NOTICE } : {}),
|
|
40
|
+
defectGuidance: defectGuidance(file.path, file.rightExists === false ? file.left.text : file.right.text),
|
|
41
|
+
snapshotId: input.id,
|
|
42
|
+
file,
|
|
43
|
+
changes,
|
|
44
|
+
context: input.context ?? [],
|
|
45
|
+
rules: input.rules ?? [],
|
|
46
|
+
};
|
|
47
|
+
|
|
22
48
|
const contextRelations = inferContextRelations(input, file);
|
|
23
49
|
if (contextRelations.length) {
|
|
24
50
|
base.contextRelations = contextRelations;
|
|
25
51
|
base.contextRelationNotice = '导入/include/命名与调用位置仅为语法线索;可能被宏、构建搜索路径或同名局部变量影响,不证明绑定、可达性或因果。context.text为批准当前/目标侧文本;仅明确oldText/oldRevision字段是历史旧侧,禁止互换。';
|
|
26
52
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
53
|
+
|
|
54
|
+
if (CPP_FILE.test(file.path)) {
|
|
55
|
+
const calls = ['old', 'new'].flatMap(side => cppCallSites((side === 'old' ? file.left : file.right).text).map(site => ({ ...site, side })));
|
|
56
|
+
if (calls.length) {
|
|
57
|
+
base.cppCallSites = calls;
|
|
58
|
+
base.cppCallNotice = '最多20条零参语法位置/侧,仅导航子集;不表示全部调用覆盖,不证明宏展开、重载选择、动态分派或执行顺序。其他源码须在批准范围内读取。';
|
|
59
|
+
}
|
|
30
60
|
}
|
|
61
|
+
|
|
31
62
|
const csharpHints = inferCsharpContext(input, file);
|
|
32
|
-
if (csharpHints.length) {
|
|
63
|
+
if (csharpHints.length) {
|
|
64
|
+
base.csharpContextHints = csharpHints;
|
|
65
|
+
base.csharpContextNotice = '仅批准context的C#静态类型/方法语法位置;历史输入按contextSide/contextVersion区分正文,无旧正文不回退目标侧。不证明语义绑定或执行可达。歧义不推定,线索不增加审查覆盖。';
|
|
66
|
+
}
|
|
67
|
+
|
|
33
68
|
if (input.callerDiscovery) {
|
|
34
69
|
const callerBindings = approvedCallerBindings(input, file);
|
|
35
|
-
if (callerBindings.length) {
|
|
70
|
+
if (callerBindings.length) {
|
|
71
|
+
base.callerBindings = callerBindings;
|
|
72
|
+
base.callerBindingNotice = '调用方文件的导入行、调用行与目标声明行仅为语法位置线索,来源限于已批准上下文;历史side/revision要求调用方与目标声明同版本,缺侧不回退。仅导航,窗口中的原文须按相应版本catalog读取;不证明执行可达、动态分派、重载选择或完整调用覆盖,也不代表全部调用方。';
|
|
73
|
+
}
|
|
36
74
|
}
|
|
75
|
+
|
|
37
76
|
let payload = JSON.stringify(base);
|
|
38
77
|
const measure = value => initialInputBudget({ instructions, input: value }, scope, maxInputBytes);
|
|
39
78
|
const metadata = {};
|
|
40
|
-
|
|
79
|
+
|
|
41
80
|
// Optional member navigation must not block source review or force extra windows.
|
|
42
|
-
if (base.contextRelations?.some(e=>e.bindingHints?.some(optional)) && !measure(payload).fits) {
|
|
81
|
+
if (base.contextRelations?.some(e => e.bindingHints?.some(optional)) && !measure(payload).fits) {
|
|
43
82
|
metadata.optionalBindingFallback = 'input-budget';
|
|
44
|
-
metadata.omittedOptionalBindingCount = base.contextRelations.reduce((n,e)=>n+(e.bindingHints??[]).filter(optional).length,0);
|
|
45
|
-
base.contextRelations = base.contextRelations.map(e=>({...e,bindingHints:(e.bindingHints??[]).filter(h
|
|
83
|
+
metadata.omittedOptionalBindingCount = base.contextRelations.reduce((n, e) => n + (e.bindingHints ?? []).filter(optional).length, 0);
|
|
84
|
+
base.contextRelations = base.contextRelations.map(e => ({ ...e, bindingHints: (e.bindingHints ?? []).filter(h => !optional(h)) }));
|
|
46
85
|
payload = JSON.stringify(base);
|
|
47
86
|
}
|
|
48
87
|
// Optional navigation must not block source review or force extra windows.
|
|
49
88
|
if (base.callerBindings?.length && !measure(payload).fits) {
|
|
50
89
|
metadata.callerBindingFallback = 'input-budget';
|
|
51
|
-
metadata.omittedCallerBindingCount = base.callerBindings.reduce((count,c)=>count+c.bindings.length,0);
|
|
52
|
-
delete base.callerBindings;
|
|
90
|
+
metadata.omittedCallerBindingCount = base.callerBindings.reduce((count, c) => count + c.bindings.length, 0);
|
|
91
|
+
delete base.callerBindings;
|
|
92
|
+
delete base.callerBindingNotice;
|
|
53
93
|
payload = JSON.stringify(base);
|
|
54
94
|
}
|
|
95
|
+
|
|
55
96
|
let batches, synthesis;
|
|
56
97
|
const group = grouping?.groups.find(g => g.fileIds.includes(file.id));
|
|
57
98
|
if (group) {
|
|
@@ -59,32 +100,48 @@ export function preparePrimaryInput(input, file, { instructions, scope, grouping
|
|
|
59
100
|
const relatedFiles = input.files.filter(f => f.id !== file.id && f.eligibility === 'reviewable' && group.fileIds.includes(f.id));
|
|
60
101
|
if (relatedFiles.length) {
|
|
61
102
|
const relations = grouping.links.filter(link => !link.split && group.fileIds.includes(link.from) && group.fileIds.includes(link.to));
|
|
62
|
-
const bindingRelations = inferImportRelations(input.files)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
103
|
+
const bindingRelations = inferImportRelations(input.files)
|
|
104
|
+
.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))
|
|
105
|
+
.map(({ from, to, side, bindingHints, definitionHints }) => ({ from, to, side, bindingHints, ...(definitionHints?.length ? { definitionHints } : {}) }));
|
|
106
|
+
const grouped = JSON.stringify({ ...base, relatedFiles, relations, bindingRelations, relationNotice: RELATION_NOTICE });
|
|
107
|
+
if (measure(grouped).fits) {
|
|
108
|
+
payload = grouped;
|
|
109
|
+
metadata.relatedFileIds = relatedFiles.map(f => f.id);
|
|
110
|
+
} else {
|
|
66
111
|
// Optional member hints must not cost the grouped related sources either.
|
|
67
112
|
const groupedOmitted = bindingRelations.reduce((n, e) => n + (e.bindingHints ?? []).filter(optional).length, 0);
|
|
68
113
|
const trimmedRelations = bindingRelations.map(e => ({ ...e, bindingHints: (e.bindingHints ?? []).filter(h => !optional(h)) })).filter(e => e.bindingHints.length || e.definitionHints?.length);
|
|
69
|
-
const trimmed = JSON.stringify({ ...base, relatedFiles, relations, bindingRelations: trimmedRelations, relationNotice:
|
|
70
|
-
if (groupedOmitted && measure(trimmed).fits) {
|
|
71
|
-
|
|
114
|
+
const trimmed = JSON.stringify({ ...base, relatedFiles, relations, bindingRelations: trimmedRelations, relationNotice: RELATION_NOTICE });
|
|
115
|
+
if (groupedOmitted && measure(trimmed).fits) {
|
|
116
|
+
payload = trimmed;
|
|
117
|
+
metadata.relatedFileIds = relatedFiles.map(f => f.id);
|
|
118
|
+
metadata.optionalBindingFallback = 'input-budget';
|
|
119
|
+
metadata.omittedOptionalBindingCount = (metadata.omittedOptionalBindingCount ?? 0) + groupedOmitted;
|
|
120
|
+
} else {
|
|
121
|
+
metadata.groupFallback = 'input-budget';
|
|
122
|
+
}
|
|
72
123
|
}
|
|
73
124
|
}
|
|
74
125
|
}
|
|
126
|
+
|
|
75
127
|
if (!measure(payload).fits && scope) {
|
|
76
|
-
const exactWindows=changeWindows(file,changes);
|
|
77
|
-
const windows=exactWindows?sourceSegments(exactWindows):null;
|
|
128
|
+
const exactWindows = changeWindows(file, changes);
|
|
129
|
+
const windows = exactWindows ? sourceSegments(exactWindows) : null;
|
|
78
130
|
if (windows) {
|
|
79
131
|
const compactFile = { ...file, left: { ...file.left, text: undefined }, right: { ...file.right, text: undefined } };
|
|
80
132
|
const contextCatalog = scope.catalog().filter(source => ['context', 'context-old'].includes(source.side));
|
|
81
133
|
if (contextCatalog.length) metadata.contextMode = 'approved-retrieval';
|
|
82
134
|
const windowBase = { ...base };
|
|
83
135
|
if (base.cppCallSites?.length) {
|
|
84
|
-
windowBase.cppCallSites = base.cppCallSites.map(({line,expression,side})=>({line,expression,side}));
|
|
136
|
+
windowBase.cppCallSites = base.cppCallSites.map(({ line, expression, side }) => ({ line, expression, side }));
|
|
85
137
|
windowBase.cppCallNotice = '最多40条旧/新侧零参语法导航;统一confidence=syntax-only-unresolved,可能为宏/重载/动态分派,无绑定、可达性或顺序保证,不是证据或完整覆盖。原文仍须按批准catalog读取。';
|
|
86
138
|
}
|
|
87
|
-
const compact = JSON.stringify({
|
|
139
|
+
const compact = JSON.stringify({
|
|
140
|
+
...windowBase, context: [], contextCatalog, contextMode: 'approved-retrieval',
|
|
141
|
+
contextNotice: '上下文全文仍在同一批准快照,仅目录进入窗口初始输入;必须按sourceId和版本侧read原文,不把目录视为证据或完整覆盖。',
|
|
142
|
+
file: compactFile, windows, sourceMode: 'change-windows',
|
|
143
|
+
windowNotice: '窗口可能为精确hunk的分段或粗粒度包络,精度以changes.precision为准;片段两侧不保证行对齐,不以片段证明精确编辑或回归因果。start/count为原始源码行号,不得重编号。完整源码未随初始输入发送,可在catalog批准范围内检索;缺少上下文时说明限制。',
|
|
144
|
+
});
|
|
88
145
|
if (!measure(compact).fits) {
|
|
89
146
|
const envelope = JSON.parse(compact);
|
|
90
147
|
const plan = planWindowBatches(windows, subset => JSON.stringify({ ...envelope, windows: subset, windowNotice: '本批仅审查windows包含的编辑,其他hunk另批;行号为原始行号,跨片段上下文不足说明限制。' }), measure);
|
|
@@ -105,11 +162,13 @@ export function preparePrimaryInput(input, file, { instructions, scope, grouping
|
|
|
105
162
|
}
|
|
106
163
|
}
|
|
107
164
|
}
|
|
165
|
+
|
|
108
166
|
if (batches?.length > 1) {
|
|
109
167
|
synthesis = prepareWindowSynthesis(batches.map(batch => batch.payload), measure);
|
|
110
168
|
metadata.synthesisStatus = synthesis.budget.fits ? 'ready' : 'input-blocked';
|
|
111
169
|
metadata.synthesisInputBytes = synthesis.budget.bytes;
|
|
112
170
|
}
|
|
171
|
+
|
|
113
172
|
const budget = measure(payload);
|
|
114
173
|
return { changes, payload, batches, synthesis, budget, metadata: { ...metadata, initialInputBytes: budget.bytes } };
|
|
115
174
|
}
|
package/src/project-report.mjs
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
import {markdownReport} from './review.mjs';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { markdownReport } from './review.mjs';
|
|
2
|
+
|
|
3
|
+
function dedupeFindings(findings) {
|
|
4
|
+
const seen = new Set();
|
|
5
|
+
const result = [];
|
|
6
|
+
for (const f of findings ?? []) {
|
|
7
|
+
const key = JSON.stringify([f.path, f.anchor, f.title, f.evidence]);
|
|
8
|
+
if (!seen.has(key)) { seen.add(key); result.push(f); }
|
|
9
|
+
}
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function renderProjectReport(report) {
|
|
14
|
+
const merged = {
|
|
15
|
+
...report,
|
|
16
|
+
findings: dedupeFindings(report.findings),
|
|
17
|
+
interactions: (report.batches ?? []).flatMap(b => b.interactions ?? []),
|
|
18
|
+
limitations: (report.batches ?? []).flatMap(b => b.limitations ?? []),
|
|
19
|
+
};
|
|
20
|
+
const scope = '总体范围:' + report.coverage.completed + '/' + report.coverage.total + ' 个文件完成主审;排除 ' + report.coverage.excluded + ',阻断 ' + report.coverage.blocked + ',未完成 ' + report.coverage.pending + '。综合未完成项单独列出;主审完成不等于审核通过。';
|
|
21
|
+
const outdated = report.outdated ? '\n注意:来源已变化或无法复核,仅对应原批准快照。' : '';
|
|
22
|
+
return markdownReport(merged).replace('# Code Review 报告', '# 全项目代码审查报告') + '\n\n' + scope + outdated;
|
|
6
23
|
}
|