@wuyanbin/ai-code-review-cli-remote 1.0.28 → 1.0.30
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/dist/constants.d.ts +7 -6
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +8 -14
- package/dist/constants.js.map +1 -1
- package/dist/core/context.d.ts +7 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +34 -16
- package/dist/core/context.js.map +1 -1
- package/dist/core/diff.d.ts +58 -1
- package/dist/core/diff.d.ts.map +1 -1
- package/dist/core/diff.js +202 -57
- package/dist/core/diff.js.map +1 -1
- package/dist/core/incremental.d.ts +68 -1
- package/dist/core/incremental.d.ts.map +1 -1
- package/dist/core/incremental.js +396 -69
- package/dist/core/incremental.js.map +1 -1
- package/dist/core/model.d.ts +23 -1
- package/dist/core/model.d.ts.map +1 -1
- package/dist/core/model.js +107 -10
- package/dist/core/model.js.map +1 -1
- package/dist/core/prompt.d.ts +10 -2
- package/dist/core/prompt.d.ts.map +1 -1
- package/dist/core/prompt.js +39 -24
- package/dist/core/prompt.js.map +1 -1
- package/dist/core/revalidation.d.ts +23 -0
- package/dist/core/revalidation.d.ts.map +1 -0
- package/dist/core/revalidation.js +150 -0
- package/dist/core/revalidation.js.map +1 -0
- package/dist/core/reviewer.d.ts +11 -2
- package/dist/core/reviewer.d.ts.map +1 -1
- package/dist/core/reviewer.js +116 -14
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/semantic-match.d.ts +29 -0
- package/dist/core/semantic-match.d.ts.map +1 -0
- package/dist/core/semantic-match.js +150 -0
- package/dist/core/semantic-match.js.map +1 -0
- package/dist/core/summarizer.d.ts.map +1 -1
- package/dist/core/summarizer.js +1 -1
- package/dist/core/summarizer.js.map +1 -1
- package/dist/gitlab/api.d.ts +42 -0
- package/dist/gitlab/api.d.ts.map +1 -1
- package/dist/gitlab/api.js +120 -23
- package/dist/gitlab/api.js.map +1 -1
- package/dist/platform/report.d.ts +3 -0
- package/dist/platform/report.d.ts.map +1 -1
- package/dist/platform/report.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +121 -28
- package/dist/runner.js.map +1 -1
- package/dist/shared/identity.d.ts +5 -0
- package/dist/shared/identity.d.ts.map +1 -1
- package/dist/shared/identity.js +11 -0
- package/dist/shared/identity.js.map +1 -1
- package/dist/shared/model-json.d.ts +6 -0
- package/dist/shared/model-json.d.ts.map +1 -0
- package/dist/shared/model-json.js +19 -0
- package/dist/shared/model-json.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -3
|
@@ -1,4 +1,28 @@
|
|
|
1
|
-
import type { AIComment, KnownFinding, ReconciledFinding } from "../types";
|
|
1
|
+
import type { AIComment, KnownFinding, ReconciledFinding, RevalidationCandidate, RevalidationVerdict } from "../types";
|
|
2
|
+
export interface ChangedLineRange {
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
}
|
|
6
|
+
export interface IncrementalChangeScope {
|
|
7
|
+
changedFiles: Set<string>;
|
|
8
|
+
/** `git diff --unified=3` 在新文件一侧的 hunk 行区间。 */
|
|
9
|
+
hunks: Map<string, ChangedLineRange[]>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 解析 lastReviewedSha..当前评审 head 的 unified diff。hunk header 的新文件范围已经
|
|
13
|
+
* 包含 `--unified=3` 上下文,可直接用作“历史 finding 附近发生修改”的边界。
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseIncrementalChangeScope(diff: string): IncrementalChangeScope;
|
|
16
|
+
export declare function isLineInChangedHunk(scope: IncrementalChangeScope, file: string, line: number): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* 加载 sinceSha..HEAD 的文件集合与带上下文 hunk。sha 不存在、force-push、
|
|
19
|
+
* 浅克隆等任何失败均返回 null,由对账层执行保守降级。
|
|
20
|
+
*/
|
|
21
|
+
export declare function loadIncrementalChangeScope(sinceSha: string, options?: {
|
|
22
|
+
cwd?: string;
|
|
23
|
+
headSha?: string;
|
|
24
|
+
contextLines?: number;
|
|
25
|
+
}): Promise<IncrementalChangeScope | null>;
|
|
2
26
|
export interface ReconcileInput {
|
|
3
27
|
/** 平台已知 findings(OPEN / WONT_FIX) */
|
|
4
28
|
known: KnownFinding[];
|
|
@@ -11,6 +35,11 @@ export interface ReconcileInput {
|
|
|
11
35
|
* null 表示无法计算(force-push / 浅克隆),新 finding 一律 blocking(安全优先)。
|
|
12
36
|
*/
|
|
13
37
|
changedFiles: Set<string> | null;
|
|
38
|
+
/**
|
|
39
|
+
* 自上次评审以来的新侧 hunk 范围;undefined 兼容旧调用,null 表示范围不可用。
|
|
40
|
+
* 历史 finding 只在可靠锚点落入 hunk 时进行修复重验证。
|
|
41
|
+
*/
|
|
42
|
+
changeScope?: IncrementalChangeScope | null;
|
|
14
43
|
/** 首轮评审(无 lastReviewedSha):所有新 finding 均为 baseline,blocking */
|
|
15
44
|
baseline: boolean;
|
|
16
45
|
/** GitLab 上被人工 resolve 的 fingerprint 集合 */
|
|
@@ -23,12 +52,33 @@ export interface ReconcileResult {
|
|
|
23
52
|
activeComments: AIComment[];
|
|
24
53
|
/** OPEN 且 blocking,作为准入判定输入 */
|
|
25
54
|
blockingComments: AIComment[];
|
|
55
|
+
/**
|
|
56
|
+
* 以「未触碰区域漏报」理由保活的 finding:几何判定无法收口,
|
|
57
|
+
* 交由模型复核裁决(判定 FIXED 时通过 applyRevalidationVerdicts 收口)。
|
|
58
|
+
*/
|
|
59
|
+
revalidationCandidates: RevalidationCandidate[];
|
|
60
|
+
/**
|
|
61
|
+
* 本轮真正新建的 finding(仅 newComments 产物,不含 severity 迁移记录):
|
|
62
|
+
* 语义判重的新报侧候选(见 applySemanticMatches)。
|
|
63
|
+
*/
|
|
64
|
+
newFindings: ReconciledFinding[];
|
|
26
65
|
stats: {
|
|
27
66
|
new: number;
|
|
28
67
|
kept: number;
|
|
29
68
|
fixed: number;
|
|
30
69
|
wontFix: number;
|
|
31
70
|
dropped: number;
|
|
71
|
+
keptReported: number;
|
|
72
|
+
keptUntouched: number;
|
|
73
|
+
fixedRevalidated: number;
|
|
74
|
+
fixedMissingAnchor: number;
|
|
75
|
+
severityMigrated: number;
|
|
76
|
+
ambiguousAnchor: number;
|
|
77
|
+
revalidatedFixed: number;
|
|
78
|
+
revalidatedPresent: number;
|
|
79
|
+
revalidatedUnsure: number;
|
|
80
|
+
semanticWontFix: number;
|
|
81
|
+
semanticDuplicate: number;
|
|
32
82
|
};
|
|
33
83
|
}
|
|
34
84
|
/**
|
|
@@ -40,11 +90,28 @@ export interface ReconcileResult {
|
|
|
40
90
|
* 不参与准入),以保证同 commit 重跑幂等、避免模型非确定性补报造成"打地鼠"。
|
|
41
91
|
*/
|
|
42
92
|
export declare function reconcileFindings(input: ReconcileInput): ReconcileResult;
|
|
93
|
+
/**
|
|
94
|
+
* 将模型复核判定应用到对账结果:仅对复核候选且判定为 FIXED 的 finding 收口,
|
|
95
|
+
* PRESENT / UNSURE / 判定缺失一律保活,使复核失败时行为与未引入复核完全一致。
|
|
96
|
+
*/
|
|
97
|
+
export declare function applyRevalidationVerdicts(result: ReconcileResult, verdicts: Map<string, RevalidationVerdict>): ReconcileResult;
|
|
98
|
+
export interface SemanticMatch {
|
|
99
|
+
newFingerprint: string;
|
|
100
|
+
knownFingerprint: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 将语义判重命中应用到对账结果:命中 WONT_FIX 的新报以新指纹落库为终态
|
|
104
|
+
* (终态跨指纹粘性),命中仍存活 OPEN 的新报按重复整体移除;命中方本轮已
|
|
105
|
+
* 收口(FIXED)时不压制(问题迁移按新 finding 处理)。每条新报至多处置一次,
|
|
106
|
+
* WONT_FIX 优先。
|
|
107
|
+
*/
|
|
108
|
+
export declare function applySemanticMatches(result: ReconcileResult, matches: SemanticMatch[]): ReconcileResult;
|
|
43
109
|
/**
|
|
44
110
|
* 计算 sinceSha..HEAD 改动的文件集合(normalize 后路径)。
|
|
45
111
|
* sha 不存在(force-push / 浅克隆)等任何失败都返回 null,由调用方按全量 blocking 处理。
|
|
46
112
|
*/
|
|
47
113
|
export declare function listChangedFiles(sinceSha: string, options?: {
|
|
48
114
|
cwd?: string;
|
|
115
|
+
headSha?: string;
|
|
49
116
|
}): Promise<Set<string> | null>;
|
|
50
117
|
//# sourceMappingURL=incremental.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"incremental.d.ts","sourceRoot":"","sources":["../../src/core/incremental.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,SAAS,EACT,YAAY,EACZ,iBAAiB,
|
|
1
|
+
{"version":3,"file":"incremental.d.ts","sourceRoot":"","sources":["../../src/core/incremental.ts"],"names":[],"mappings":"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"}
|
package/dist/core/incremental.js
CHANGED
|
@@ -1,12 +1,135 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseIncrementalChangeScope = parseIncrementalChangeScope;
|
|
4
|
+
exports.isLineInChangedHunk = isLineInChangedHunk;
|
|
5
|
+
exports.loadIncrementalChangeScope = loadIncrementalChangeScope;
|
|
3
6
|
exports.reconcileFindings = reconcileFindings;
|
|
7
|
+
exports.applyRevalidationVerdicts = applyRevalidationVerdicts;
|
|
8
|
+
exports.applySemanticMatches = applySemanticMatches;
|
|
4
9
|
exports.listChangedFiles = listChangedFiles;
|
|
5
10
|
const node_child_process_1 = require("node:child_process");
|
|
6
11
|
const node_util_1 = require("node:util");
|
|
7
12
|
const diff_1 = require("./diff");
|
|
8
13
|
const identity_1 = require("../shared/identity");
|
|
9
14
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
15
|
+
/**
|
|
16
|
+
* 解析 lastReviewedSha..当前评审 head 的 unified diff。hunk header 的新文件范围已经
|
|
17
|
+
* 包含 `--unified=3` 上下文,可直接用作“历史 finding 附近发生修改”的边界。
|
|
18
|
+
*/
|
|
19
|
+
function parseIncrementalChangeScope(diff) {
|
|
20
|
+
const changedFiles = new Set((0, diff_1.getChangedFiles)(diff).map(diff_1.normalizePath));
|
|
21
|
+
const hunks = new Map();
|
|
22
|
+
let currentFile = null;
|
|
23
|
+
for (const raw of diff.split("\n")) {
|
|
24
|
+
if (raw.startsWith("diff --git ")) {
|
|
25
|
+
currentFile = null;
|
|
26
|
+
const match = /^diff --git a\/(.+?) b\/(.+)$/.exec(raw);
|
|
27
|
+
if (match)
|
|
28
|
+
currentFile = (0, diff_1.normalizePath)(match[2]);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (raw.startsWith("+++ b/")) {
|
|
32
|
+
currentFile = (0, diff_1.normalizePath)(raw.slice("+++ b/".length));
|
|
33
|
+
changedFiles.add(currentFile);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (!currentFile || !raw.startsWith("@@"))
|
|
37
|
+
continue;
|
|
38
|
+
const match = /^@@\s+\-\d+(?:,\d+)?\s+\+(\d+)(?:,(\d+))?\s+@@/.exec(raw);
|
|
39
|
+
if (!match)
|
|
40
|
+
continue;
|
|
41
|
+
const start = Number(match[1]);
|
|
42
|
+
const count = match[2] === undefined ? 1 : Number(match[2]);
|
|
43
|
+
if (!Number.isFinite(start) || !Number.isFinite(count) || count < 0) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const ranges = hunks.get(currentFile) ?? [];
|
|
47
|
+
if (count === 0) {
|
|
48
|
+
// 纯删除:新侧无行,删除点位于 start 与 start+1 之间,
|
|
49
|
+
// 取该邻域,使"用删除代码修复"同样算作附近已修改。
|
|
50
|
+
ranges.push({ start: Math.max(1, start), end: start + 1 });
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
ranges.push({ start, end: start + count - 1 });
|
|
54
|
+
}
|
|
55
|
+
hunks.set(currentFile, ranges);
|
|
56
|
+
}
|
|
57
|
+
return { changedFiles, hunks };
|
|
58
|
+
}
|
|
59
|
+
function isLineInChangedHunk(scope, file, line) {
|
|
60
|
+
const ranges = scope.hunks.get((0, diff_1.normalizePath)(file));
|
|
61
|
+
return (ranges?.some((range) => line >= range.start && line <= range.end) ?? false);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 「附近」的上下文半径。几何判定只作为快路径:半径之外的语义修复由模型复核
|
|
65
|
+
* (revalidation)裁决,因此无需再按项目调参。
|
|
66
|
+
*/
|
|
67
|
+
const DEFAULT_INCREMENTAL_CONTEXT = 3;
|
|
68
|
+
const GIT_EXEC_OPTIONS = { maxBuffer: 10 * 1024 * 1024 };
|
|
69
|
+
async function hasCommit(sha, cwd) {
|
|
70
|
+
try {
|
|
71
|
+
await execFileAsync("git", ["cat-file", "-e", `${sha}^{commit}`], {
|
|
72
|
+
...GIT_EXEC_OPTIONS,
|
|
73
|
+
cwd,
|
|
74
|
+
});
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 浅克隆(GIT_DEPTH)或 force-push 后 lastReviewedSha 可能不在本地历史中,
|
|
83
|
+
* 此时增量范围会退化为不可用并让已修复 finding 一直保活。先探测,缺失时
|
|
84
|
+
* 逐级加深,仍取不到才放弃。
|
|
85
|
+
*/
|
|
86
|
+
async function ensureCommitAvailable(sha, cwd) {
|
|
87
|
+
if (await hasCommit(sha, cwd))
|
|
88
|
+
return true;
|
|
89
|
+
const attempts = [
|
|
90
|
+
["fetch", "--deepen=200", "--quiet"],
|
|
91
|
+
["fetch", "--unshallow", "--quiet"],
|
|
92
|
+
];
|
|
93
|
+
for (const args of attempts) {
|
|
94
|
+
try {
|
|
95
|
+
await execFileAsync("git", args, { ...GIT_EXEC_OPTIONS, cwd });
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// 非浅克隆、无远端或无权限时该次加深无意义,继续尝试下一种
|
|
99
|
+
}
|
|
100
|
+
if (await hasCommit(sha, cwd)) {
|
|
101
|
+
console.error(`[findings] deepened local history to reach lastReviewedSha via "git ${args.join(" ")}"`);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 加载 sinceSha..HEAD 的文件集合与带上下文 hunk。sha 不存在、force-push、
|
|
109
|
+
* 浅克隆等任何失败均返回 null,由对账层执行保守降级。
|
|
110
|
+
*/
|
|
111
|
+
async function loadIncrementalChangeScope(sinceSha, options = {}) {
|
|
112
|
+
if (!(await ensureCommitAvailable(sinceSha, options.cwd))) {
|
|
113
|
+
console.error(`[findings] incremental diff unavailable: lastReviewedSha ${sinceSha} not reachable locally (shallow clone or force-push), keeping reliable known findings OPEN`);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const contextLines = options.contextLines ?? DEFAULT_INCREMENTAL_CONTEXT;
|
|
117
|
+
try {
|
|
118
|
+
const headSha = options.headSha?.trim() || "HEAD";
|
|
119
|
+
const { stdout } = await execFileAsync("git", [
|
|
120
|
+
"diff",
|
|
121
|
+
`--unified=${contextLines}`,
|
|
122
|
+
`${sinceSha}..${headSha}`,
|
|
123
|
+
"--",
|
|
124
|
+
], { ...GIT_EXEC_OPTIONS, cwd: options.cwd });
|
|
125
|
+
return parseIncrementalChangeScope(stdout);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.error(`[findings] incremental diff unavailable (${error instanceof Error ? error.message.split("\n")[0] : error}), treating all new findings as blocking`);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const SEVERITY_RANK = { P0: 2, P1: 1, P2: 0 };
|
|
10
133
|
function codeKey(file, code) {
|
|
11
134
|
const trimmed = code?.trim();
|
|
12
135
|
if (!trimmed)
|
|
@@ -25,6 +148,42 @@ function toComment(finding) {
|
|
|
25
148
|
: {}),
|
|
26
149
|
};
|
|
27
150
|
}
|
|
151
|
+
function resolveHistoricalAnchor(added, finding) {
|
|
152
|
+
const code = finding.code?.trim();
|
|
153
|
+
if (!code)
|
|
154
|
+
return { kind: "missing" };
|
|
155
|
+
const lines = added.get((0, diff_1.normalizePath)(finding.file));
|
|
156
|
+
if (!lines?.length)
|
|
157
|
+
return { kind: "missing" };
|
|
158
|
+
const exact = lines.find((item) => item.line === finding.line);
|
|
159
|
+
if (exact?.content.trim() === code) {
|
|
160
|
+
return { kind: "exact", line: exact.line, code: exact.content };
|
|
161
|
+
}
|
|
162
|
+
const matches = lines.filter((item) => item.content.trim() === code);
|
|
163
|
+
if (matches.length === 1) {
|
|
164
|
+
return {
|
|
165
|
+
kind: "unique",
|
|
166
|
+
line: matches[0].line,
|
|
167
|
+
code: matches[0].content,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return { kind: matches.length > 1 ? "ambiguous" : "missing" };
|
|
171
|
+
}
|
|
172
|
+
function findingFromComment(comment) {
|
|
173
|
+
return {
|
|
174
|
+
fingerprint: (0, identity_1.buildCommentFingerprint)(comment),
|
|
175
|
+
file: (0, diff_1.normalizePath)(comment.file),
|
|
176
|
+
line: comment.line,
|
|
177
|
+
severity: comment.severity,
|
|
178
|
+
message: comment.message,
|
|
179
|
+
status: "OPEN",
|
|
180
|
+
blocking: true,
|
|
181
|
+
...(comment.code !== undefined ? { code: comment.code } : {}),
|
|
182
|
+
...(comment.suggestion !== undefined
|
|
183
|
+
? { suggestion: comment.suggestion }
|
|
184
|
+
: {}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
28
187
|
/**
|
|
29
188
|
* 将本次模型输出与已知 finding 集合对账,得到收敛后的状态全集。
|
|
30
189
|
*
|
|
@@ -35,56 +194,89 @@ function toComment(finding) {
|
|
|
35
194
|
*/
|
|
36
195
|
function reconcileFindings(input) {
|
|
37
196
|
const added = (0, diff_1.collectAddedLines)(input.diff);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
};
|
|
48
|
-
// 模型未返回 code 时从 diff 新增行回填,保证 fingerprint 稳定、下次可比对
|
|
49
|
-
const current = input.current.map((comment) => {
|
|
50
|
-
if (comment.code?.trim())
|
|
51
|
-
return comment;
|
|
52
|
-
const lines = added.get((0, diff_1.normalizePath)(comment.file));
|
|
53
|
-
const hit = lines?.find((item) => item.line === comment.line);
|
|
54
|
-
return hit ? { ...comment, code: hit.content } : comment;
|
|
55
|
-
});
|
|
56
|
-
// 双级索引:fingerprint 精确匹配优先,file+code 兜底(容忍 severity 翻动)
|
|
197
|
+
// 不按行号回填 code:对齐层已保证 accepted 评论携带 diff 真实行内容,
|
|
198
|
+
// 仅凭行号补 code 等于重新引入无证据锚点。
|
|
199
|
+
const current = input.current;
|
|
200
|
+
const anchors = new Map(input.known.map((finding) => [
|
|
201
|
+
finding.fingerprint,
|
|
202
|
+
resolveHistoricalAnchor(added, finding),
|
|
203
|
+
]));
|
|
204
|
+
// 双级索引:fingerprint 精确匹配优先,file+code 只在历史 finding 可唯一
|
|
205
|
+
// 配对时兜底。current 已经过严格新增行对齐,可作为显式复报证据。
|
|
57
206
|
const byFingerprint = new Map();
|
|
58
207
|
const byCode = new Map();
|
|
59
208
|
for (const finding of input.known) {
|
|
60
209
|
byFingerprint.set(finding.fingerprint, finding);
|
|
61
210
|
const key = codeKey(finding.file, finding.code);
|
|
62
|
-
if (key
|
|
63
|
-
byCode.
|
|
211
|
+
if (key) {
|
|
212
|
+
const candidates = byCode.get(key) ?? [];
|
|
213
|
+
candidates.push(finding);
|
|
214
|
+
byCode.set(key, candidates);
|
|
215
|
+
}
|
|
64
216
|
}
|
|
65
217
|
const matchedFingerprints = new Set();
|
|
66
218
|
const matchedCurrent = new Map();
|
|
67
|
-
const
|
|
68
|
-
|
|
219
|
+
const consumed = new Set();
|
|
220
|
+
// 同一 known 只能被一条 current 占位,因此先让高 severity 参与配对:
|
|
221
|
+
// 否则谁占位取决于模型输出顺序,可能静默丢弃更严重的评论。
|
|
222
|
+
const matchOrder = [...current].sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
|
|
223
|
+
for (const comment of matchOrder) {
|
|
69
224
|
const fingerprint = (0, identity_1.buildCommentFingerprint)(comment);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
225
|
+
const exactFingerprint = byFingerprint.get(fingerprint);
|
|
226
|
+
const candidates = exactFingerprint
|
|
227
|
+
? [exactFingerprint]
|
|
228
|
+
: byCode.get(codeKey(comment.file, comment.code) ?? "\0") ?? [];
|
|
229
|
+
const available = candidates.filter((candidate) => !matchedFingerprints.has(candidate.fingerprint));
|
|
230
|
+
const sameLine = available.filter((candidate) => {
|
|
231
|
+
const anchor = anchors.get(candidate.fingerprint);
|
|
232
|
+
return (anchor &&
|
|
233
|
+
(anchor.kind === "exact" || anchor.kind === "unique") &&
|
|
234
|
+
anchor.line === comment.line);
|
|
235
|
+
});
|
|
236
|
+
const match = (exactFingerprint &&
|
|
237
|
+
!matchedFingerprints.has(exactFingerprint.fingerprint)
|
|
238
|
+
? exactFingerprint
|
|
239
|
+
: null) ??
|
|
240
|
+
(sameLine.length === 1 ? sameLine[0] : null) ??
|
|
241
|
+
(available.length === 1 ? available[0] : null);
|
|
73
242
|
if (match) {
|
|
74
243
|
// 同一 known 被多条 current 命中时首条生效,重复项丢弃
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
244
|
+
matchedFingerprints.add(match.fingerprint);
|
|
245
|
+
matchedCurrent.set(match.fingerprint, comment);
|
|
246
|
+
consumed.add(comment);
|
|
79
247
|
}
|
|
80
|
-
else {
|
|
81
|
-
|
|
248
|
+
else if (candidates.length > 0 && available.length === 0) {
|
|
249
|
+
// 同一 known 的重复 current 已被更高 severity 的评论消费。
|
|
250
|
+
consumed.add(comment);
|
|
82
251
|
}
|
|
83
252
|
}
|
|
253
|
+
// 保持模型输出顺序,使同 commit 重跑的 finding 序列稳定
|
|
254
|
+
const newComments = current.filter((comment) => !consumed.has(comment));
|
|
84
255
|
const findings = [];
|
|
85
|
-
const
|
|
256
|
+
const revalidationCandidates = [];
|
|
257
|
+
const stats = {
|
|
258
|
+
new: 0,
|
|
259
|
+
kept: 0,
|
|
260
|
+
fixed: 0,
|
|
261
|
+
wontFix: 0,
|
|
262
|
+
dropped: 0,
|
|
263
|
+
keptReported: 0,
|
|
264
|
+
keptUntouched: 0,
|
|
265
|
+
fixedRevalidated: 0,
|
|
266
|
+
fixedMissingAnchor: 0,
|
|
267
|
+
severityMigrated: 0,
|
|
268
|
+
ambiguousAnchor: 0,
|
|
269
|
+
revalidatedFixed: 0,
|
|
270
|
+
revalidatedPresent: 0,
|
|
271
|
+
revalidatedUnsure: 0,
|
|
272
|
+
semanticWontFix: 0,
|
|
273
|
+
semanticDuplicate: 0,
|
|
274
|
+
};
|
|
86
275
|
for (const known of input.known) {
|
|
87
276
|
const base = { ...known };
|
|
277
|
+
const anchor = anchors.get(known.fingerprint) ?? { kind: "missing" };
|
|
278
|
+
if (anchor.kind === "ambiguous")
|
|
279
|
+
stats.ambiguousAnchor += 1;
|
|
88
280
|
if (known.status === "WONT_FIX" || input.humanResolved.has(known.fingerprint)) {
|
|
89
281
|
// 人工 dismiss 终态:即使模型再次报告也不复活
|
|
90
282
|
findings.push({ ...base, status: "WONT_FIX" });
|
|
@@ -93,30 +285,87 @@ function reconcileFindings(input) {
|
|
|
93
285
|
}
|
|
94
286
|
const reported = matchedCurrent.get(known.fingerprint);
|
|
95
287
|
if (reported) {
|
|
96
|
-
|
|
288
|
+
const touched = input.changeScope !== null &&
|
|
289
|
+
input.changeScope !== undefined &&
|
|
290
|
+
isLineInChangedHunk(input.changeScope, reported.file, reported.line);
|
|
291
|
+
if (reported.severity !== known.severity && touched) {
|
|
292
|
+
// severity 属于 fingerprint;迁移必须收口旧记录并创建新记录,
|
|
293
|
+
// 不能在旧 fingerprint 下原地改变 severity。
|
|
294
|
+
findings.push({ ...base, status: "FIXED" });
|
|
295
|
+
findings.push(findingFromComment(reported));
|
|
296
|
+
stats.fixed += 1;
|
|
297
|
+
stats.severityMigrated += 1;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (reported.severity !== known.severity) {
|
|
301
|
+
// 未修改区域的 severity 抖动不覆盖历史状态或文案。
|
|
302
|
+
findings.push({
|
|
303
|
+
...base,
|
|
304
|
+
status: "OPEN",
|
|
305
|
+
file: (0, diff_1.normalizePath)(reported.file),
|
|
306
|
+
line: reported.line,
|
|
307
|
+
...(reported.code !== undefined ? { code: reported.code } : {}),
|
|
308
|
+
});
|
|
309
|
+
stats.kept += 1;
|
|
310
|
+
stats.keptUntouched += 1;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
// 同 severity 明确复报:行号、真实 code 与文案取本次可靠结果。
|
|
97
314
|
findings.push({
|
|
98
315
|
...base,
|
|
99
316
|
status: "OPEN",
|
|
317
|
+
file: (0, diff_1.normalizePath)(reported.file),
|
|
100
318
|
line: reported.line,
|
|
319
|
+
...(reported.code !== undefined ? { code: reported.code } : {}),
|
|
101
320
|
message: reported.message,
|
|
102
321
|
...(reported.suggestion !== undefined
|
|
103
322
|
? { suggestion: reported.suggestion }
|
|
104
323
|
: {}),
|
|
105
324
|
});
|
|
106
325
|
stats.kept += 1;
|
|
326
|
+
stats.keptReported += 1;
|
|
107
327
|
continue;
|
|
108
328
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
stats.
|
|
329
|
+
if (anchor.kind === "ambiguous" || anchor.kind === "missing") {
|
|
330
|
+
// 无法可靠定位时不允许任取第一行保活。
|
|
331
|
+
findings.push({ ...base, status: "FIXED" });
|
|
332
|
+
stats.fixed += 1;
|
|
333
|
+
stats.fixedMissingAnchor += 1;
|
|
114
334
|
continue;
|
|
115
335
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
336
|
+
const touched = input.changeScope !== null &&
|
|
337
|
+
input.changeScope !== undefined &&
|
|
338
|
+
isLineInChangedHunk(input.changeScope, known.file, anchor.line);
|
|
339
|
+
if (touched) {
|
|
340
|
+
// 用户修改了锚点附近且模型未复报:按复核结果收口。
|
|
341
|
+
findings.push({ ...base, status: "FIXED" });
|
|
342
|
+
stats.fixed += 1;
|
|
343
|
+
stats.fixedRevalidated += 1;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
// 未触碰区域或增量范围未知:保守视为模型漏报并保活,
|
|
347
|
+
// 同时进入模型复核候选,由复核裁决是否收口(见 applyRevalidationVerdicts)。
|
|
348
|
+
findings.push({
|
|
349
|
+
...base,
|
|
350
|
+
status: "OPEN",
|
|
351
|
+
line: anchor.line,
|
|
352
|
+
code: anchor.code,
|
|
353
|
+
});
|
|
354
|
+
revalidationCandidates.push({
|
|
355
|
+
fingerprint: known.fingerprint,
|
|
356
|
+
file: (0, diff_1.normalizePath)(known.file),
|
|
357
|
+
line: anchor.line,
|
|
358
|
+
code: anchor.code,
|
|
359
|
+
severity: known.severity,
|
|
360
|
+
message: known.message,
|
|
361
|
+
...(known.suggestion !== undefined
|
|
362
|
+
? { suggestion: known.suggestion }
|
|
363
|
+
: {}),
|
|
364
|
+
});
|
|
365
|
+
stats.kept += 1;
|
|
366
|
+
stats.keptUntouched += 1;
|
|
119
367
|
}
|
|
368
|
+
const newFindings = [];
|
|
120
369
|
for (const comment of newComments) {
|
|
121
370
|
const file = (0, diff_1.normalizePath)(comment.file);
|
|
122
371
|
const inScope = input.baseline ||
|
|
@@ -127,28 +376,117 @@ function reconcileFindings(input) {
|
|
|
127
376
|
stats.dropped += 1;
|
|
128
377
|
continue;
|
|
129
378
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
line: comment.line,
|
|
134
|
-
severity: comment.severity,
|
|
135
|
-
message: comment.message,
|
|
136
|
-
status: "OPEN",
|
|
137
|
-
blocking: true,
|
|
138
|
-
...(comment.code !== undefined ? { code: comment.code } : {}),
|
|
139
|
-
...(comment.suggestion !== undefined
|
|
140
|
-
? { suggestion: comment.suggestion }
|
|
141
|
-
: {}),
|
|
142
|
-
});
|
|
379
|
+
const created = findingFromComment({ ...comment, file });
|
|
380
|
+
findings.push(created);
|
|
381
|
+
newFindings.push(created);
|
|
143
382
|
stats.new += 1;
|
|
144
383
|
}
|
|
384
|
+
return {
|
|
385
|
+
findings,
|
|
386
|
+
...buildCommentSets(findings),
|
|
387
|
+
revalidationCandidates,
|
|
388
|
+
newFindings,
|
|
389
|
+
stats,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function buildCommentSets(findings) {
|
|
145
393
|
const open = findings
|
|
146
394
|
.filter((f) => f.status === "OPEN")
|
|
147
395
|
.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
148
396
|
return {
|
|
149
|
-
findings,
|
|
150
397
|
activeComments: open.map(toComment),
|
|
151
398
|
blockingComments: open.filter((f) => f.blocking).map(toComment),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* 将模型复核判定应用到对账结果:仅对复核候选且判定为 FIXED 的 finding 收口,
|
|
403
|
+
* PRESENT / UNSURE / 判定缺失一律保活,使复核失败时行为与未引入复核完全一致。
|
|
404
|
+
*/
|
|
405
|
+
function applyRevalidationVerdicts(result, verdicts) {
|
|
406
|
+
if (verdicts.size === 0)
|
|
407
|
+
return result;
|
|
408
|
+
const candidates = new Set(result.revalidationCandidates.map((c) => c.fingerprint));
|
|
409
|
+
const stats = { ...result.stats };
|
|
410
|
+
const findings = result.findings.map((finding) => {
|
|
411
|
+
if (finding.status !== "OPEN" || !candidates.has(finding.fingerprint)) {
|
|
412
|
+
return finding;
|
|
413
|
+
}
|
|
414
|
+
const verdict = verdicts.get(finding.fingerprint);
|
|
415
|
+
if (verdict === "FIXED") {
|
|
416
|
+
stats.revalidatedFixed += 1;
|
|
417
|
+
stats.fixed += 1;
|
|
418
|
+
stats.kept -= 1;
|
|
419
|
+
stats.keptUntouched -= 1;
|
|
420
|
+
return { ...finding, status: "FIXED" };
|
|
421
|
+
}
|
|
422
|
+
if (verdict === "PRESENT")
|
|
423
|
+
stats.revalidatedPresent += 1;
|
|
424
|
+
else if (verdict === "UNSURE")
|
|
425
|
+
stats.revalidatedUnsure += 1;
|
|
426
|
+
return finding;
|
|
427
|
+
});
|
|
428
|
+
return {
|
|
429
|
+
findings,
|
|
430
|
+
...buildCommentSets(findings),
|
|
431
|
+
revalidationCandidates: result.revalidationCandidates,
|
|
432
|
+
newFindings: result.newFindings,
|
|
433
|
+
stats,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* 将语义判重命中应用到对账结果:命中 WONT_FIX 的新报以新指纹落库为终态
|
|
438
|
+
* (终态跨指纹粘性),命中仍存活 OPEN 的新报按重复整体移除;命中方本轮已
|
|
439
|
+
* 收口(FIXED)时不压制(问题迁移按新 finding 处理)。每条新报至多处置一次,
|
|
440
|
+
* WONT_FIX 优先。
|
|
441
|
+
*/
|
|
442
|
+
function applySemanticMatches(result, matches) {
|
|
443
|
+
if (matches.length === 0)
|
|
444
|
+
return result;
|
|
445
|
+
const byFingerprint = new Map(result.findings.map((finding) => [finding.fingerprint, finding]));
|
|
446
|
+
const newFingerprints = new Set(result.newFindings.map((finding) => finding.fingerprint));
|
|
447
|
+
const statusRank = (match) => byFingerprint.get(match.knownFingerprint)?.status === "WONT_FIX" ? 0 : 1;
|
|
448
|
+
const ordered = [...matches].sort((a, b) => statusRank(a) - statusRank(b));
|
|
449
|
+
const handled = new Set();
|
|
450
|
+
const toWontFix = new Set();
|
|
451
|
+
const toDrop = new Set();
|
|
452
|
+
const stats = { ...result.stats };
|
|
453
|
+
for (const match of ordered) {
|
|
454
|
+
if (handled.has(match.newFingerprint))
|
|
455
|
+
continue;
|
|
456
|
+
if (!newFingerprints.has(match.newFingerprint))
|
|
457
|
+
continue;
|
|
458
|
+
const target = byFingerprint.get(match.newFingerprint);
|
|
459
|
+
const known = byFingerprint.get(match.knownFingerprint);
|
|
460
|
+
if (!target || target.status !== "OPEN" || !known)
|
|
461
|
+
continue;
|
|
462
|
+
if (known.status === "WONT_FIX") {
|
|
463
|
+
toWontFix.add(match.newFingerprint);
|
|
464
|
+
handled.add(match.newFingerprint);
|
|
465
|
+
stats.new -= 1;
|
|
466
|
+
stats.wontFix += 1;
|
|
467
|
+
stats.semanticWontFix += 1;
|
|
468
|
+
}
|
|
469
|
+
else if (known.status === "OPEN") {
|
|
470
|
+
toDrop.add(match.newFingerprint);
|
|
471
|
+
handled.add(match.newFingerprint);
|
|
472
|
+
stats.new -= 1;
|
|
473
|
+
stats.semanticDuplicate += 1;
|
|
474
|
+
}
|
|
475
|
+
// known 已 FIXED(本轮复核收口):问题迁移,新报放行
|
|
476
|
+
}
|
|
477
|
+
if (handled.size === 0)
|
|
478
|
+
return result;
|
|
479
|
+
const findings = result.findings
|
|
480
|
+
.filter((finding) => !toDrop.has(finding.fingerprint))
|
|
481
|
+
.map((finding) => toWontFix.has(finding.fingerprint)
|
|
482
|
+
? { ...finding, status: "WONT_FIX" }
|
|
483
|
+
: finding);
|
|
484
|
+
const newFindings = result.newFindings.filter((finding) => !handled.has(finding.fingerprint));
|
|
485
|
+
return {
|
|
486
|
+
findings,
|
|
487
|
+
...buildCommentSets(findings),
|
|
488
|
+
revalidationCandidates: result.revalidationCandidates,
|
|
489
|
+
newFindings,
|
|
152
490
|
stats,
|
|
153
491
|
};
|
|
154
492
|
}
|
|
@@ -157,18 +495,7 @@ function reconcileFindings(input) {
|
|
|
157
495
|
* sha 不存在(force-push / 浅克隆)等任何失败都返回 null,由调用方按全量 blocking 处理。
|
|
158
496
|
*/
|
|
159
497
|
async function listChangedFiles(sinceSha, options = {}) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const files = stdout
|
|
163
|
-
.split("\n")
|
|
164
|
-
.map((line) => line.trim())
|
|
165
|
-
.filter(Boolean)
|
|
166
|
-
.map(diff_1.normalizePath);
|
|
167
|
-
return new Set(files);
|
|
168
|
-
}
|
|
169
|
-
catch (error) {
|
|
170
|
-
console.error(`[findings] incremental diff unavailable (${error instanceof Error ? error.message.split("\n")[0] : error}), treating all new findings as blocking`);
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
498
|
+
const scope = await loadIncrementalChangeScope(sinceSha, options);
|
|
499
|
+
return scope?.changedFiles ?? null;
|
|
173
500
|
}
|
|
174
501
|
//# sourceMappingURL=incremental.js.map
|