progmune-runtime 3.7.21 → 3.7.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.22] — 2026-09-06
4
+
5
+ ### Java 方法学三连——恢复率裁决 + 真实闭环 + 名碰撞消除
6
+
7
+ - **提取器恢复率裁决**(tree-sitter-java AST 基准,spring-realworld):修复三根因(参数注解实参括号 34、通配符泛型 3、构造器 2)+ 调用侧修复(泛型构造调用/注释剔除/多行点链/泛型静态调用)——方法 100%(178/178)、调用边 100%(1216/1216)、0 FP。**词法路线成立,无需 JavaParser 依赖**(与 C/Go 同级别裁决)
8
+ - **真实语料协议行标注闭环**(`REALWORLD_JAVA_ANNOTATION_V1.md`):3 注解 + 1 别名端到端——v1 token 合法流 0 违规 + 摘抽取步反证精确报出;**v2 捕获真实 TP**(spring-realworld `updateUser` 密码明文入库——1581★ 参考实现真实 bug,改密后无法登录;修复变异违规即消);v3 语料无手工资源管理(MyBatis 托管)维持合成金标
9
+ - **接收者限定名匹配(名碰撞根因修复)**:提取器 `className` 捕获(嵌套类感知类名栈)+ 完整点链调用输出;注册层 `Class.method` 限定键(裸名键仅项目内唯一时注册);匹配层大小写不敏感限定精确匹配(带点调用跳过规范化/词段形态);变量名≠类名(`jwtService.` vs `DefaultJwtService`)走项目别名(注解合并后重校验加载)——**1 真实 TP / 0 误报**(9 名碰撞消除)
10
+ - **P4.6 内联深度恢复**:call-sequence 限定调用末段回退解析(同文件优先 + 接收者-类名双向后缀偏好;规则保留集大小写不敏感前置)——违规归因上移到入口(updateProfile),与 TS/Python「helper 违规归因到入口 flow」语义对齐
11
+ - **Spring 现代方言真实语料验证**(`REALWORLD_SPRING_V2.md`,ali-bouali/spring-boot-3-jwt-security,Boot 3 教程标杆):SecurityFilterChain bean + requestMatchers 静态导入 + **String[] 变量白名单展开**(原保守跳过致公开 mutation 被兜底掩盖漏报)+ auth 词段豁免修复(authenticate/refresh-token)——15 路由 0 issues、摘兜底反证 5 重现、V1 语料复扫无回归
12
+ - **audit:realworld fastapi 考核入口修复**(模块名推导 py→fastapi,该框架自工具建立即不可用)+ 全框架语料复扫 14/14 零回归
13
+ - 发布门:套件 255 green;Python 盲测 64 / TS 795 / C 应用金标 F1=95.7% 全部零漂移;check 免疫正常(.progmune_allowlist 存量补录)
14
+
15
+ ## [3.7.15 – 3.7.21] — 2026-09-04/05
16
+
17
+ ### 框架真实语料 12/12 收官 + Java/Spring 语言支持 + Java 协议行引擎化
18
+
19
+ - **框架检测器真实语料全量收官(12/12)**:Fiber 真实生产语料考核(journalist 多层 Register 链传播引擎 12 FP→0 + 反证 9 重现;jiotv_go 能力令牌形态标注)——8 启发式 + 4 结构级全部真实语料验证到 0 协议级 FP + 敏感性反证;CLAUDE.md 转正待办清空
20
+ - **audit:realworld 一键考核工具 + 证据档位制**:任意真实开源项目 clone/vendor → 扫描 → JSON 报告 → 金标标注模板(docs/REALWORLD_METHODOLOGY.md)
21
+ - **Java/Spring 语言支持(3.7.17-18)**:`extract-ir-java.ts` 提取器(纯 TS 词法)+ Spring 路由覆盖模型(19 路由/12 mutation 全解析、0 issues、anyRequest 兜底翻转反证 10 重现)+ 调用图提取 + SSG 方向决策(`REALWORLD_SPRING_V1.md`)
22
+ - **Java 协议行引擎化(3.7.19-21)**:协议行金标 v1 + Spring 方言扩展(requestMatchers/类级 @PreAuthorize);token 生命周期 / auth-register 哈希先行 / resource 管理(invalidate + 泄漏端)三族引擎回归锁定(docs/java-language-status.md)
23
+
3
24
  ## [3.7.14] — 2026-09-02
4
25
 
5
26
  ### Go 提取器修复 + 恢复率裁决 + 框架诚实分层 + 三语料实验
@@ -68,6 +68,7 @@ function buildCallSequences(ir, keepNames, maxCalls = exports.MAX_SEQUENCE_CALLS
68
68
  // 跨文件函数指针分发仍不可见(L3 结论不变)。
69
69
  const fnMap = new Map();
70
70
  const fnMapByFile = new Map();
71
+ const fnMapAll = new Map(); // 裸名 → 全部定义(限定回退候选)
71
72
  for (const f of ir) {
72
73
  if (!isProjectFn(f))
73
74
  continue;
@@ -79,9 +80,55 @@ function buildCallSequences(ir, keepNames, maxCalls = exports.MAX_SEQUENCE_CALLS
79
80
  }
80
81
  if (!byFile.has(f.name))
81
82
  byFile.set(f.name, f);
83
+ const all = fnMapAll.get(f.name) || [];
84
+ all.push(f);
85
+ fnMapAll.set(f.name, all);
82
86
  }
83
- /** 调用解析:同文件定义优先,全局按名回退(跨文件同名时每个文件绑自己的) */
84
- const resolveCall = (fromFile, name) => fnMapByFile.get(fromFile)?.get(name) ?? fnMap.get(name);
87
+ // 保留集小写副本:限定调用名大小写不敏感命中(Java 规则键
88
+ // User.update vs 变量调用 user.update——大小写差异下的保留判定)
89
+ const keepNamesLower = keepNames ? new Set([...keepNames].map((k) => k.toLowerCase())) : undefined;
90
+ /** 限定名末段回退(Java 接收者限定输出):同文件优先;全局候选
91
+ * 偏好 className 与接收者变量名大小写不敏感后缀匹配(user→User、
92
+ * userRepository→MyBatisUserRepository、jwtService→DefaultJwtService
93
+ * ——Java 字段名=类名 camelCase 的惯例);无偏好时 last-wins 兜底 */
94
+ const resolveQualified = (fromFile, name) => {
95
+ const dot = name.lastIndexOf(".");
96
+ const seg = name.slice(dot + 1);
97
+ if (!seg)
98
+ return undefined;
99
+ const byFile = fnMapByFile.get(fromFile)?.get(seg);
100
+ if (byFile)
101
+ return byFile;
102
+ const all = fnMapAll.get(seg) || [];
103
+ if (all.length === 0)
104
+ return undefined;
105
+ if (all.length === 1)
106
+ return all[0];
107
+ const recv = name.slice(0, dot).toLowerCase();
108
+ // 双向后缀:变量名=类名去前缀(userRepository→MyBatisUserRepository、
109
+ // jwtService→DefaultJwtService——类名以接收者结尾)或变量名带类名
110
+ // (类名以接收者结尾的场景罕见,保留)
111
+ const hit = recv
112
+ ? all.find((f) => {
113
+ if (!f.className)
114
+ return false;
115
+ const cn = f.className.toLowerCase();
116
+ return recv === cn || cn.endsWith(recv) || recv.endsWith(cn);
117
+ })
118
+ : undefined;
119
+ return hit ?? fnMap.get(seg);
120
+ };
121
+ /** 调用解析:同文件定义优先,全局按名回退(跨文件同名时每个文件绑自己的);
122
+ * 限定名精确不中时末段回退——无注解项目 helper 的 P4.6 内联深度恢复
123
+ * (Java 限定化前此类调用按裸名解析,限定化后曾退化为外部 token) */
124
+ const resolveCall = (fromFile, name) => {
125
+ const exact = fnMapByFile.get(fromFile)?.get(name) ?? fnMap.get(name);
126
+ if (exact)
127
+ return exact;
128
+ if (!name.includes("."))
129
+ return undefined;
130
+ return resolveQualified(fromFile, name);
131
+ };
85
132
  // 被项目函数调用过的函数不是入口(其片段并入调用方展开序列)——
86
133
  // 按 文件+名字 粒度判定,避免 A 文件的 static x 被 B 文件的调用误判非入口
87
134
  const fnKey = (file, name) => file + "::" + name;
@@ -119,9 +166,16 @@ function buildCallSequences(ir, keepNames, maxCalls = exports.MAX_SEQUENCE_CALLS
119
166
  const expandCall = (name, depth, visiting, fromFile) => {
120
167
  if (depth > MAX_DEPTH || visiting.has(name))
121
168
  return [];
169
+ // 规则函数保留优先(先于解析):限定调用名大小写不敏感命中保留集
170
+ // → 保留 token 不解析不内联(解析会经末段回退内联掉规则 token,
171
+ // 且可能绑到同名异类定义)
172
+ if (keepNames && (keepNames.has(name) || (name.includes(".") && keepNamesLower.has(name.toLowerCase())))) {
173
+ budget.left--;
174
+ return [name];
175
+ }
122
176
  const fn = resolveCall(fromFile, name);
123
- // 外部调用或规则函数:调用名保留给匹配层,不内联(记 1 个调用)
124
- if (!fn || (keepNames && keepNames.has(name))) {
177
+ // 外部调用:调用名保留给匹配层,不内联(记 1 个调用)
178
+ if (!fn) {
125
179
  budget.left--;
126
180
  return [name];
127
181
  }
@@ -137,3 +137,58 @@ function fn(name, file, calls) {
137
137
  (0, vitest_1.expect)((0, call_sequence_1.collectProjectFunctionNames)([external]).size).toBe(0);
138
138
  });
139
139
  });
140
+ // ── 限定调用末段回退解析(Java 接收者限定输出,2026-09-06)──
141
+ /** 带 className 的最小 FunctionInfo(Java 接收者限定语义) */
142
+ function fnQ(name, file, calls, className) {
143
+ return { name, params: [], returnType: "void", file, calls, exported: true, external: false, className };
144
+ }
145
+ (0, vitest_1.describe)("buildCallSequences(限定调用回退解析)", () => {
146
+ (0, vitest_1.it)("限定调用末段回退:无注解项目 helper 经接收者解析后被内联(P4.6 深度恢复)", () => {
147
+ const ir = [
148
+ fnQ("save", "MyBatisUserRepository.java", ["insertUser"], "MyBatisUserRepository"),
149
+ fnQ("insertUser", "MyBatisUserRepository.java", ["db.execute"]),
150
+ fnQ("updateUser", "UserService.java", ["userRepository.save"]),
151
+ ];
152
+ const seqs = (0, call_sequence_1.buildCallSequences)(ir);
153
+ (0, vitest_1.expect)(seqs).toHaveLength(1);
154
+ (0, vitest_1.expect)(seqs[0].function).toBe("updateUser");
155
+ // save 被解析并内联 → insertUser(叶子)保留
156
+ (0, vitest_1.expect)(seqs[0].calls).toEqual(["insertUser"]);
157
+ });
158
+ (0, vitest_1.it)("限定调用命中保留集(大小写不敏感):规则 token 不解析不内联", () => {
159
+ const ir = [
160
+ fnQ("update", "User.java", ["audit"], "User"),
161
+ fnQ("audit", "User.java", ["db.write"]),
162
+ fnQ("updateUser", "UserService.java", ["user.update"]),
163
+ ];
164
+ // 规则键 = 类限定(User.update);调用 = 变量限定(user.update)——大小写差异
165
+ const seqs = (0, call_sequence_1.buildCallSequences)(ir, new Set(["User.update"]));
166
+ (0, vitest_1.expect)(seqs).toHaveLength(1);
167
+ (0, vitest_1.expect)(seqs[0].function).toBe("updateUser");
168
+ // 保留 token 而非内联(audit 不得出现)
169
+ (0, vitest_1.expect)(seqs[0].calls).toEqual(["user.update"]);
170
+ });
171
+ (0, vitest_1.it)("接收者-类名后缀偏好:同名 save 绑定到接收者对应类(userRepository → MyBatisUserRepository)", () => {
172
+ const ir = [
173
+ fnQ("save", "MyBatisUserRepository.java", ["insertUser"], "MyBatisUserRepository"),
174
+ fnQ("save", "MyBatisArticleRepository.java", ["insertArticle"], "MyBatisArticleRepository"),
175
+ fnQ("insertUser", "MyBatisUserRepository.java", ["db.execute"]),
176
+ fnQ("insertArticle", "MyBatisArticleRepository.java", ["db.execute"]),
177
+ fnQ("updateUser", "UserService.java", ["userRepository.save"]),
178
+ fnQ("updateArticle", "ArticleCommandService.java", ["articleRepository.save"]),
179
+ ];
180
+ const seqs = (0, call_sequence_1.buildCallSequences)(ir);
181
+ const byFn = new Map(seqs.map((s) => [s.function, s.calls]));
182
+ (0, vitest_1.expect)(byFn.get("updateUser")).toEqual(["insertUser"]);
183
+ (0, vitest_1.expect)(byFn.get("updateArticle")).toEqual(["insertArticle"]);
184
+ });
185
+ (0, vitest_1.it)("入口判定:仅被限定名调用的函数不是入口", () => {
186
+ const ir = [
187
+ fnQ("save", "MyBatisUserRepository.java", ["db.execute"]),
188
+ fnQ("updateUser", "UserService.java", ["userRepository.save"]),
189
+ ];
190
+ const seqs = (0, call_sequence_1.buildCallSequences)(ir);
191
+ (0, vitest_1.expect)(seqs.filter((s) => s.function === "save")).toHaveLength(0);
192
+ (0, vitest_1.expect)(seqs).toHaveLength(1);
193
+ });
194
+ });
@@ -96,6 +96,116 @@ function collectPrecedingComment(code, matchIndex) {
96
96
  }
97
97
  return pieces.join("\n");
98
98
  }
99
+ /**
100
+ * 类名栈扫描:逐字符维护括号深度(字符串/注释感知),产出按字符位置
101
+ * 索引的当前类名表(null = 匿名类/方法体/块内)。'{' 前回溯 token:
102
+ * class|interface|enum|record Name → 命名类;new X(...) { 匿名类等 → null。
103
+ * 方法声明位置取栈顶为 className(嵌套类感知——JacksonCustomizations
104
+ * 内 DateTimeSerializer 等方法归属正确的嵌套类)。
105
+ */
106
+ function buildClassStack(code) {
107
+ const stack = [null]; // 文件顶层
108
+ const at = new Array(code.length).fill(null);
109
+ let i = 0;
110
+ while (i < code.length) {
111
+ at[i] = stack[stack.length - 1];
112
+ const ch = code[i];
113
+ const next = code[i + 1];
114
+ if (ch === "/" && next === "/") {
115
+ i += 2;
116
+ while (i < code.length && code[i] !== "\n")
117
+ i++;
118
+ continue;
119
+ }
120
+ if (ch === "/" && next === "*") {
121
+ i += 2;
122
+ while (i < code.length && !(code[i] === "*" && code[i + 1] === "/"))
123
+ i++;
124
+ i += 2;
125
+ continue;
126
+ }
127
+ if (ch === '"' || ch === "'") {
128
+ // 跳字符串/字符字面量(含转义)
129
+ const q = ch;
130
+ i++;
131
+ while (i < code.length) {
132
+ if (code[i] === "\\") {
133
+ i += 2;
134
+ continue;
135
+ }
136
+ if (code[i] === q) {
137
+ i++;
138
+ break;
139
+ }
140
+ i++;
141
+ }
142
+ continue;
143
+ }
144
+ if (ch === "{") {
145
+ // 判定 '{' 打开什么:回溯最近类声明(允许 record Name(...) 组件形态)
146
+ const before = code.slice(Math.max(0, i - 200), i);
147
+ const clsM = before.match(/\b(class|interface|enum|record)\s+([A-Za-z_$]\w*)[^{]*$/);
148
+ stack.push(clsM ? clsM[2] : null);
149
+ i++;
150
+ continue;
151
+ }
152
+ if (ch === "}") {
153
+ if (stack.length > 1)
154
+ stack.pop();
155
+ i++;
156
+ continue;
157
+ }
158
+ i++;
159
+ }
160
+ return at;
161
+ }
162
+ /** 剔除行注释与块注释(字符串感知)——注释里的标识符不是调用 */
163
+ function stripJavaComments(code) {
164
+ let out = "";
165
+ let i = 0;
166
+ while (i < code.length) {
167
+ const ch = code[i];
168
+ const next = code[i + 1];
169
+ if (ch === "/" && next === "/") {
170
+ i += 2;
171
+ while (i < code.length && code[i] !== "\n")
172
+ i++;
173
+ out += "\n";
174
+ continue;
175
+ }
176
+ if (ch === "/" && next === "*") {
177
+ i += 2;
178
+ while (i < code.length && !(code[i] === "*" && code[i + 1] === "/"))
179
+ i++;
180
+ i += 2;
181
+ out += " ";
182
+ continue;
183
+ }
184
+ if (ch === '"' || ch === "'") {
185
+ const q = ch;
186
+ out += ch;
187
+ i++;
188
+ while (i < code.length) {
189
+ out += code[i];
190
+ if (code[i] === "\\") {
191
+ i++;
192
+ out += code[i];
193
+ i++;
194
+ continue;
195
+ }
196
+ if (code[i] === q) {
197
+ i++;
198
+ break;
199
+ }
200
+ i++;
201
+ }
202
+ continue;
203
+ }
204
+ out += ch;
205
+ i++;
206
+ }
207
+ return out;
208
+ }
99
209
  /** 单文件提取 */
100
210
  function extractJavaFile(filePath) {
101
211
  const out = [];
@@ -106,42 +216,85 @@ function extractJavaFile(filePath) {
106
216
  catch {
107
217
  return out;
108
218
  }
109
- // 方法声明:修饰符 + 返回类型 名字(参数) [throws ...] {
110
- const re = /(?:\b(?:public|protected|private)\s+)?(?:(?:static|final|synchronized|abstract|default)\s+)*(?:[\w$<>\[\],.\s]+?)\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*(?:throws\s+[^{]+?)?\{/g;
219
+ const classStack = buildClassStack(code);
220
+ // 方法声明:两分支——构造器(可见性 + Name(,无返回类型)与普通方法
221
+ // (修饰符 + 返回类型 Name(,返回类型含 '?' 通配符如 ResponseEntity<?>)。
222
+ // 匹配止于 '(';参数区用字符串感知平衡括号扫描(@PathVariable("slug")
223
+ // 等注解实参的内层括号不再截断参数列表);throws 子句与 '{' 手扫确认。
224
+ const re = /(?:\b(public|protected|private)\s+([A-Za-z_$][\w$]*)\s*\(|(?:\b(?:public|protected|private)\s+)?(?:(?:static|final|synchronized|abstract|default)\s+)*(?:[\w$<>\[\],.?\s]+?)\s+([A-Za-z_$][\w$]*)\s*\()/g;
111
225
  let m;
112
226
  while ((m = re.exec(code)) !== null) {
113
- const name = m[1];
227
+ const isCtor = m[1] !== undefined;
228
+ const name = isCtor ? m[2] : m[3];
114
229
  if (JAVA_KEYWORDS.has(name))
115
230
  continue;
231
+ const parenPos = m.index + m[0].length - 1; // 匹配止于 '('
116
232
  // 重叠守卫:匹配起点若早于参数 '(' 所在行(如从上一行注释/注解开吃
117
233
  // 吞掉真头)→ 从 '(' 行首重扫,避免方法被错配候选吞掉
118
- const parenPos = code.indexOf("(", m.index + m[0].indexOf(name));
119
234
  const parenLineStart = code.lastIndexOf("\n", parenPos - 1) + 1;
120
235
  if (m.index < parenLineStart) {
121
236
  re.lastIndex = parenLineStart;
122
237
  continue;
123
238
  }
124
- // 起点规则:'.' 前导 = 方法调用(如 adminService.act( 的 act);
125
- // 注解行起点(@RequestMapping…)非方法声明;容忍注释内起点——
126
- // 其名字/参数/体边界本就正确(正则 type-part 可吞注释空白)
239
+ // 起点规则:'.' 前导 = 方法调用(如 adminService.act( 的 act)。
240
+ // 注:'@'/'='/'"' 不在 type-part 字符类内,注解文本本身不可能成为
241
+ // 匹配起点,无需旧版 @ 行守卫(@Autowired public UserService( 的
242
+ // 匹配起于 public,行首是 @——旧守卫会误杀同行注解构造器)
127
243
  const pre = code.slice(Math.max(0, m.index - 1), m.index);
128
244
  if (pre === ".")
129
245
  continue;
130
- const lineStart = code.lastIndexOf("\n", m.index - 1) + 1;
131
- const curLine = code.slice(lineStart, code.indexOf("\n", lineStart) === -1 ? code.length : code.indexOf("\n", lineStart)).trim();
132
- if (curLine.startsWith("@") && !curLine.startsWith("@protocol") && !curLine.startsWith("@progmune"))
133
- continue;
246
+ // 参数区:字符串感知平衡括号扫描
247
+ let pDepth = 1;
248
+ let k = parenPos + 1;
249
+ let quote = null;
250
+ while (k < code.length && pDepth > 0) {
251
+ const ch = code[k];
252
+ if (quote) {
253
+ if (ch === quote && code[k - 1] !== "\\")
254
+ quote = null;
255
+ }
256
+ else if (ch === '"' || ch === "'") {
257
+ quote = ch;
258
+ }
259
+ else if (ch === "(") {
260
+ pDepth++;
261
+ }
262
+ else if (ch === ")") {
263
+ pDepth--;
264
+ }
265
+ k++;
266
+ }
267
+ if (pDepth !== 0)
268
+ continue; // 括号不闭合
269
+ const rawParams = code.slice(parenPos + 1, k - 1);
270
+ // throws 子句 + '{' 手扫确认(候选非方法声明时在此丢弃)
271
+ let t = k;
272
+ while (t < code.length && /\s/.test(code[t]))
273
+ t++;
274
+ let bodyStart;
275
+ if (code.slice(t, t + 6) === "throws" && !/[\w$]/.test(code[t + 6] || "")) {
276
+ const brace = code.indexOf("{", t);
277
+ if (brace === -1)
278
+ continue;
279
+ if (/;/.test(code.slice(t, brace)))
280
+ continue; // throws 到 { 之间有 ; = 调用行
281
+ bodyStart = brace + 1;
282
+ }
283
+ else {
284
+ if (code[t] !== "{")
285
+ continue;
286
+ bodyStart = t + 1;
287
+ }
134
288
  const head = code.slice(m.index, m.index + 40);
135
289
  const visM = head.match(/\b(public|protected|private)\b/);
136
290
  const exported = visM ? visM[1] !== "private" : /^interface\s/.test(code.slice(0, m.index).split("\n").pop() || "") ? true : undefined;
137
291
  const params = [];
138
- const rawParams = m[2].trim();
139
- if (rawParams.length > 0) {
292
+ if (rawParams.trim().length > 0) {
140
293
  for (const raw of rawParams.split(",")) {
141
- const t = raw.trim();
142
- if (!t)
294
+ const t2 = raw.trim();
295
+ if (!t2)
143
296
  continue;
144
- const parts = t.split(/\s+/);
297
+ const parts = t2.split(/\s+/);
145
298
  const pname = parts.pop() || "";
146
299
  const ptype = parts.join(" ") || "?";
147
300
  if (/^[A-Za-z_$][\w$]*$/.test(pname) && pname !== "final") {
@@ -155,16 +308,14 @@ function extractJavaFile(filePath) {
155
308
  // '(' 位置定真实 header 行,避免注释归属串位)
156
309
  let protocol;
157
310
  {
158
- const parenIdx = code.indexOf("(", m.index);
159
- const joined = parenIdx === -1 ? "" : collectPrecedingComment(code, parenIdx);
311
+ const joined = collectPrecedingComment(code, parenPos);
160
312
  const lastAt = Math.max(joined.lastIndexOf("@protocol"), joined.lastIndexOf("@progmune"));
161
313
  if (lastAt !== -1) {
162
314
  protocol = parseJavaProtocol(joined.slice(lastAt, Math.min(joined.length, lastAt + 300)));
163
315
  }
164
316
  }
165
- // calls:方法体(自头正则消费的 '{' 起平衡到 '}')内的方法调用
317
+ // calls:方法体('{' 起平衡到 '}')内的方法调用
166
318
  const calls = [];
167
- const bodyStart = m.index + m[0].length; // 正则已含 '{'
168
319
  let depth = 1;
169
320
  let bodyEnd = bodyStart;
170
321
  while (bodyEnd < code.length && depth > 0) {
@@ -176,17 +327,28 @@ function extractJavaFile(filePath) {
176
327
  bodyEnd++;
177
328
  }
178
329
  const body = code.slice(bodyStart, Math.min(bodyEnd, bodyStart + 8000));
179
- // 零宽后视:前导 '(' 等不被上一匹配吞掉(if (verifyToken(…) 中能收到 verifyToken)
180
- const callRe = /(?<=^|[^\w$])([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\(/g;
330
+ // 注释剔除:// 注释里的 setAllowCredentials(true) 不是调用(限定化后
331
+ // 与真实调用分流,注释噪声显形——REALWORLD 恢复率复测发现)
332
+ const cleanBody = stripJavaComments(body);
333
+ // 零宽后视:前导 '(' 等不被上一匹配吞掉(if (verifyToken(…) 中能收到 verifyToken);
334
+ // <…> 可选类型实参:new HashMap<String, Object>() 的 HashMap 可见;
335
+ // 链原子容忍点两侧空白 + 泛型静态调用(DataFetcherResult.<T>newResult()):
336
+ // 接收者限定匹配的前提——REALWORLD 恢复率复测 96.6%→100%
337
+ const callRe = /(?<=^|[^\w$])([A-Za-z_$][\w$]*(?:\s*\.\s*(?:<[^<>;]*>)?\s*[A-Za-z_$][\w$]*)*)\s*(?:<[^<>;]*(?:<[^<>;]*>[^<>;]*)*>)?\s*\(/g;
181
338
  let cm;
182
- while ((cm = callRe.exec(body)) !== null) {
183
- const seg = cm[1].split(".").pop() || "";
339
+ while ((cm = callRe.exec(cleanBody)) !== null) {
340
+ // 接收者限定输出(Class.method 匹配前提):带点链保留完整、剥空白/
341
+ // 泛型实参与 this.
342
+ let full = cm[1].replace(/\s+/g, "").replace(/<[^<>]*>/g, "");
343
+ if (full.startsWith("this."))
344
+ full = full.slice(5);
345
+ const seg = full.split(".").pop() || "";
184
346
  if (JAVA_KEYWORDS.has(seg))
185
347
  continue;
186
- if (/^(if|for|while|switch|catch|new|return|case|do)$/.test(seg))
348
+ if (/^(if|for|while|switch|catch|new|return|case|do|super|this)$/.test(seg))
187
349
  continue;
188
- if (calls.length < 400 && !calls.includes(seg))
189
- calls.push(seg);
350
+ if (calls.length < 400 && !calls.includes(full))
351
+ calls.push(full);
190
352
  }
191
353
  out.push({
192
354
  name,
@@ -196,6 +358,7 @@ function extractJavaFile(filePath) {
196
358
  exported,
197
359
  calls,
198
360
  protocol,
361
+ className: classStack[m.index] || undefined,
199
362
  });
200
363
  }
201
364
  return out;
@@ -71,12 +71,15 @@ public class JwtTokenFilter extends OncePerRequestFilter {
71
71
  (0, vitest_1.expect)(fns.some((f) => f.name === "doFilterInternal")).toBe(true);
72
72
  (0, vitest_1.expect)(fns.some((f) => f.name === "getTokenString")).toBe(true);
73
73
  });
74
- (0, vitest_1.it)("方法调用边(calls)被提取——JWT 认证链可见", () => {
74
+ (0, vitest_1.it)("方法调用边(calls)被提取——JWT 认证链可见(带接收者输出完整链)", () => {
75
75
  const fp = path.join(dir, "JwtTokenFilter.java");
76
76
  fs.writeFileSync(fp, SAMPLE);
77
77
  const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
78
78
  const filter = fns.find((f) => f.name === "doFilterInternal");
79
- (0, vitest_1.expect)(filter.calls).toContain("getSubFromToken");
79
+ (0, vitest_1.expect)(filter.calls).toContain("jwtService.getSubFromToken");
80
+ // 链式调用按段输出:SecurityContextHolder.getContext() 输出第一段,
81
+ // 其返回值的 getAuthentication() 无文本接收者 → 裸名
82
+ (0, vitest_1.expect)(filter.calls).toContain("SecurityContextHolder.getContext");
80
83
  (0, vitest_1.expect)(filter.calls).toContain("getAuthentication");
81
84
  (0, vitest_1.expect)(filter.calls).toContain("setAuthentication");
82
85
  // 关键字不算调用
@@ -145,3 +148,161 @@ const MUT_FILTER = REAL_FILTER.replace(" .flatMap(token -> jwtService.get
145
148
  (0, vitest_1.expect)(tokenVerifyBeforeUse(calls).why).toContain("无 verify");
146
149
  });
147
150
  });
151
+ // ── 恢复率裁决修复回归(spring-realworld AST 基准实测根因,2026-09-05)──
152
+ (0, vitest_1.describe)("恢复率裁决修复回归(spring-realworld 实测三根因)", () => {
153
+ (0, vitest_1.it)("参数注解实参括号:@PathVariable(\"slug\") 不截断参数列表", () => {
154
+ const fp = path.join(dir, "ArticleApi.java");
155
+ fs.writeFileSync(fp, `package app;
156
+ public class ArticleApi {
157
+ @DeleteMapping
158
+ public ResponseEntity deleteArticle(
159
+ @PathVariable("slug") String slug, @AuthenticationPrincipal User user) {
160
+ return articleRepository.findBySlug(slug);
161
+ }
162
+ }`);
163
+ const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
164
+ const fn = fns.find((f) => f.name === "deleteArticle");
165
+ (0, vitest_1.expect)(fn).toBeTruthy();
166
+ (0, vitest_1.expect)(fn.params.map((p) => p.name)).toEqual(["slug", "user"]);
167
+ });
168
+ (0, vitest_1.it)("通配符泛型返回类型:ResponseEntity<?> 方法被提取", () => {
169
+ const fp = path.join(dir, "Wildcard.java");
170
+ fs.writeFileSync(fp, `package app;
171
+ public class Wildcard {
172
+ public ResponseEntity<?> article(String slug) {
173
+ return ResponseEntity.ok(slug);
174
+ }
175
+ }`);
176
+ const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
177
+ (0, vitest_1.expect)(fns.some((f) => f.name === "article")).toBe(true);
178
+ });
179
+ (0, vitest_1.it)("构造器(无返回类型):public Name(…) 被提取(含同行 @Autowired)", () => {
180
+ const fp = path.join(dir, "UserService.java");
181
+ fs.writeFileSync(fp, `package app;
182
+ public class UserService {
183
+ @Autowired
184
+ public UserService(
185
+ UserRepository userRepository,
186
+ @Value("\${image.default}") String defaultImage) {
187
+ this.userRepository = userRepository;
188
+ }
189
+ }`);
190
+ const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
191
+ (0, vitest_1.expect)(fns.some((f) => f.name === "UserService")).toBe(true);
192
+ });
193
+ (0, vitest_1.it)("泛型对象构造调用:new HashMap<String, Object>() 的 HashMap 可见", () => {
194
+ const fp = path.join(dir, "Resp.java");
195
+ fs.writeFileSync(fp, `package app;
196
+ public class Resp {
197
+ public Map<?, ?> build() {
198
+ Map<String, Object> m = new HashMap<String, Object>();
199
+ return m;
200
+ }
201
+ }`);
202
+ const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
203
+ const calls = fns.find((f) => f.name === "build").calls;
204
+ (0, vitest_1.expect)(calls).toContain("HashMap");
205
+ });
206
+ (0, vitest_1.it)("super/this 构造调用不算调用边", () => {
207
+ const fp = path.join(dir, "Base.java");
208
+ fs.writeFileSync(fp, `package app;
209
+ public class Base {
210
+ public Base(int config) {
211
+ super(config);
212
+ }
213
+ }`);
214
+ const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
215
+ const ctor = fns.find((f) => f.name === "Base");
216
+ (0, vitest_1.expect)(ctor).toBeTruthy();
217
+ (0, vitest_1.expect)(ctor.calls).not.toContain("super");
218
+ });
219
+ });
220
+ // ── 接收者限定名匹配(名碰撞根因修复,2026-09-06)──
221
+ (0, vitest_1.describe)("接收者限定名匹配(className 捕获 + 限定调用输出)", () => {
222
+ (0, vitest_1.it)("className 捕获:顶层类与嵌套类归属正确", () => {
223
+ const fp = path.join(dir, "JacksonCustomizations.java");
224
+ fs.writeFileSync(fp, `package app;
225
+ public class JacksonCustomizations {
226
+ public void outer() {}
227
+ public static class DateTimeSerializer {
228
+ public void serialize() {}
229
+ public static class Inner {
230
+ public void innermost() {}
231
+ }
232
+ }
233
+ public interface NestedIface {
234
+ default void ifaceMethod() {}
235
+ }
236
+ }`);
237
+ const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
238
+ const byName = new Map(fns.map((f) => [f.name, f]));
239
+ (0, vitest_1.expect)(byName.get("outer").className).toBe("JacksonCustomizations");
240
+ (0, vitest_1.expect)(byName.get("serialize").className).toBe("DateTimeSerializer");
241
+ (0, vitest_1.expect)(byName.get("innermost").className).toBe("Inner");
242
+ (0, vitest_1.expect)(byName.get("ifaceMethod").className).toBe("NestedIface");
243
+ });
244
+ (0, vitest_1.it)("匿名类内方法 className 为 undefined(按无类名处理)", () => {
245
+ const fp = path.join(dir, "Anon.java");
246
+ fs.writeFileSync(fp, `package app;
247
+ public class Anon {
248
+ public void setup() {
249
+ Runnable r = new Runnable() {
250
+ public void run() {
251
+ doWork();
252
+ }
253
+ };
254
+ }
255
+ }`);
256
+ const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
257
+ (0, vitest_1.expect)(fns.find((f) => f.name === "run").className).toBeUndefined();
258
+ (0, vitest_1.expect)(fns.find((f) => f.name === "setup").className).toBe("Anon");
259
+ });
260
+ (0, vitest_1.it)("record 声明形态:record Name(...) { 的类名被捕获", () => {
261
+ const fp = path.join(dir, "Point.java");
262
+ fs.writeFileSync(fp, `package app;
263
+ public record Point(int x, int y) {
264
+ public int sum() { return x + y; }
265
+ }`);
266
+ const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
267
+ (0, vitest_1.expect)(fns.find((f) => f.name === "sum").className).toBe("Point");
268
+ });
269
+ (0, vitest_1.it)("this. 前缀剥离:this.foo() 输出裸名", () => {
270
+ const fp = path.join(dir, "Self.java");
271
+ fs.writeFileSync(fp, `package app;
272
+ public class Self {
273
+ public void outer() {
274
+ this.inner();
275
+ }
276
+ public void inner() {}
277
+ }`);
278
+ const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
279
+ (0, vitest_1.expect)(fns.find((f) => f.name === "outer").calls).toContain("inner");
280
+ (0, vitest_1.expect)(fns.find((f) => f.name === "outer").calls).not.toContain("this.inner");
281
+ });
282
+ (0, vitest_1.it)("限定串去重:a.open() 与 b.open() 是两条不同调用边", () => {
283
+ const fp = path.join(dir, "Multi.java");
284
+ fs.writeFileSync(fp, `package app;
285
+ public class Multi {
286
+ public void go(A a, B b) {
287
+ a.open();
288
+ b.open();
289
+ }
290
+ }`);
291
+ const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
292
+ const calls = fns.find((f) => f.name === "go").calls;
293
+ (0, vitest_1.expect)(calls).toContain("a.open");
294
+ (0, vitest_1.expect)(calls).toContain("b.open");
295
+ });
296
+ (0, vitest_1.it)("无接收者调用保持裸名(protocol 金标 exact-match 锁)", () => {
297
+ const fp = path.join(dir, "Bare.java");
298
+ fs.writeFileSync(fp, `package app;
299
+ public class Bare {
300
+ public void caller() {
301
+ setAuthentication(id, request);
302
+ }
303
+ public void setAuthentication(String id, Object r) {}
304
+ }`);
305
+ const fns = (0, extract_ir_java_1.extractJavaFile)(fp);
306
+ (0, vitest_1.expect)(fns.find((f) => f.name === "caller").calls).toContain("setAuthentication");
307
+ });
308
+ });
@@ -65,7 +65,10 @@ const ACCESS_AUTH = new Set([
65
65
  "hasAuthority", "hasAnyAuthority", "rememberMe", "denyAll",
66
66
  ]);
67
67
  function isAuthEntryPath(p) {
68
- return /\/?(login|signin|signup|sign_in|register|refresh)(\/|$)/i.test(p);
68
+ // 词段前缀 + [-/] 边界:refresh-token、authenticate、token 家族均豁免
69
+ // (ali-bouali 现代语料实测:/auth/authenticate、/auth/refresh-token
70
+ // 被白名单 permitAll 命中后误报——真实登录/刷新入口,词表缺口修复)
71
+ return /\/(login|signin|signup|sign_in|register|refresh|authenticate|token)([-/]|$)/i.test(p);
69
72
  }
70
73
  /** Spring ant 模式 → 正则(** → 任意段,* → 单段) */
71
74
  function antToRegex(pattern) {
@@ -76,6 +79,22 @@ function antToRegex(pattern) {
76
79
  function parseSecurityConfig(code) {
77
80
  const rules = [];
78
81
  let catchAll = null;
82
+ // String[] 常量数组声明(现代方言标配:private static final String[]
83
+ // WHITE_LIST_URL = {...})——requestMatchers(NAME) 变量引用展开为字面量。
84
+ // 仅覆盖本文件内 String[] 形态;List.of/Set.of/跨文件常量待语料补充。
85
+ const arrayVars = new Map();
86
+ {
87
+ const arrRe = /(?:private\s+|public\s+|protected\s+)?static\s+final\s+String\[\]\s+([A-Za-z_]\w*)\s*=\s*\{/g;
88
+ let am;
89
+ while ((am = arrRe.exec(code)) !== null) {
90
+ const name = am[1];
91
+ const end = code.indexOf("};", am.index + am[0].length);
92
+ if (end === -1)
93
+ continue;
94
+ const literals = [...code.slice(am.index, end).matchAll(/"([^"]+)"/g)].map((x) => x[1]);
95
+ arrayVars.set(name, literals);
96
+ }
97
+ }
79
98
  // 逐个 .antMatchers/.requestMatchers(...) 与其后 .access() 配对
80
99
  // (旧 DSL antMatchers + 新式 SecurityFilterChain authorizeHttpRequests
81
100
  // requestMatchers——2026-09-02 Spring 方言扩展)
@@ -98,8 +117,13 @@ function parseSecurityConfig(code) {
98
117
  for (const pm of rest.matchAll(/"([^"]+)"/g))
99
118
  patterns.push(pm[1]);
100
119
  if (patterns.length === 0) {
101
- // 无引号模式(变量引用)→ 宽匹配不了,跳过(保守:不豁免)
102
- continue;
120
+ // 无引号模式(变量引用)→ String[] 常量数组展开;
121
+ // 未知变量保守跳过(不豁免)
122
+ const varName = rest.match(/^([A-Za-z_]\w*)$/);
123
+ const arr = varName ? arrayVars.get(varName[1]) : undefined;
124
+ if (!arr)
125
+ continue;
126
+ patterns.push(...arr);
103
127
  }
104
128
  rules.push({ method, patterns, access: m[2] });
105
129
  }
@@ -168,4 +168,77 @@ public class AdminApi {
168
168
  const a = (0, spring_detector_1.analyzeSpringProject)(dir);
169
169
  (0, vitest_1.expect)(a.issues.some((i) => i.route === "POST /articles")).toBe(true);
170
170
  });
171
+ (0, vitest_1.it)("String[] 白名单变量展开:非 auth 词路径 permitAll mutation 被看见(修复前被兜底掩盖)", () => {
172
+ // 现代 Boot 3 教程标配形态:变量数组白名单(ali-bouali/spring-boot-3-jwt-security)。
173
+ // 展开后 /public/** 命中 permitAll 规则 → POST /public/files 公开 mutation 被报;
174
+ // 未展开时该规则被跳过、路由落到兜底 authenticated → 漏报(保守方向错误)。
175
+ const cfg = `${PKG}
176
+ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
177
+ @Configuration @EnableWebSecurity
178
+ public class WebSecurityConfig {
179
+ private static final String[] WHITE_LIST_URL = {"/api/v1/auth/**", "/v3/api-docs", "/public/**"};
180
+ @Bean
181
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
182
+ http.authorizeHttpRequests(req -> req
183
+ .requestMatchers(WHITE_LIST_URL).permitAll()
184
+ .anyRequest().authenticated());
185
+ return http.build();
186
+ }
187
+ }`;
188
+ writeJava("sec/WebSecurityConfig.java", cfg);
189
+ writeJava("api/PublicApi.java", `${PKG}
190
+ @RestController @RequestMapping("/public")
191
+ public class PublicApi {
192
+ @PostMapping("/files") public Object up() { return null; }
193
+ }`);
194
+ const a = (0, spring_detector_1.analyzeSpringProject)(dir);
195
+ (0, vitest_1.expect)(a.issues.some((i) => i.route === "POST /public/files")).toBe(true);
196
+ });
197
+ (0, vitest_1.it)("auth 词段豁免:/auth/authenticate 与 /auth/refresh-token 公开 mutation 不报(词表缺口修复)", () => {
198
+ const cfg = `${PKG}
199
+ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
200
+ @Configuration @EnableWebSecurity
201
+ public class WebSecurityConfig {
202
+ private static final String[] WHITE_LIST_URL = {"/api/v1/auth/**"};
203
+ @Bean
204
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
205
+ http.authorizeHttpRequests(req -> req
206
+ .requestMatchers(WHITE_LIST_URL).permitAll()
207
+ .anyRequest().authenticated());
208
+ return http.build();
209
+ }
210
+ }`;
211
+ writeJava("sec/WebSecurityConfig.java", cfg);
212
+ writeJava("api/AuthApi.java", `${PKG}
213
+ @RestController @RequestMapping("/api/v1/auth")
214
+ public class AuthApi {
215
+ @PostMapping("/authenticate") public Object login() { return null; }
216
+ @PostMapping("/refresh-token") public Object refresh() { return null; }
217
+ }`);
218
+ const a = (0, spring_detector_1.analyzeSpringProject)(dir);
219
+ (0, vitest_1.expect)(a.issues).toHaveLength(0);
220
+ });
221
+ (0, vitest_1.it)("反证:白名单变量改名(展开失明)→ 路由落到兜底 authenticated,公开 mutation 漏报", () => {
222
+ const cfg = `${PKG}
223
+ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
224
+ @Configuration @EnableWebSecurity
225
+ public class WebSecurityConfig {
226
+ private static final String[] RENAMED = {"/public/**"};
227
+ @Bean
228
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
229
+ http.authorizeHttpRequests(req -> req
230
+ .requestMatchers(WHITE_LIST_URL).permitAll()
231
+ .anyRequest().authenticated());
232
+ return http.build();
233
+ }
234
+ }`;
235
+ writeJava("sec/WebSecurityConfig.java", cfg);
236
+ writeJava("api/PublicApi.java", `${PKG}
237
+ @RestController @RequestMapping("/public")
238
+ public class PublicApi {
239
+ @PostMapping("/files") public Object up() { return null; }
240
+ }`);
241
+ const a = (0, spring_detector_1.analyzeSpringProject)(dir);
242
+ (0, vitest_1.expect)(a.issues).toHaveLength(0);
243
+ });
171
244
  });
package/dist/sdk.js CHANGED
@@ -20,7 +20,7 @@ const risk_model_1 = require("./risk-model");
20
20
  const protocol_knowledge_1 = require("./protocol-knowledge");
21
21
  const evidence_repository_1 = require("./evidence-repository");
22
22
  /** Runtime version — stable public identifier. Internal layers evolve underneath. */
23
- exports.RUNTIME_VERSION = "3.7.21";
23
+ exports.RUNTIME_VERSION = "3.7.22";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
@@ -1248,23 +1248,42 @@ async function collectProtocolViolations(ctx, callGraph) {
1248
1248
  // ir.json 两种形态:extractIR/extractIRPython 的裸数组、extractProjectIR
1249
1249
  // 的 { typeMap, functions } 合并对象(execute/MCP 写盘)——统一取函数列表。
1250
1250
  const functions = Array.isArray(ir) ? ir : (ir.functions || []);
1251
+ // 裸名出现次数(Java 接收者限定匹配:碰撞名不注册裸键——
1252
+ // User.update 与 Article.update 同名时裸键 "update" 会命中所有
1253
+ // .update( 调用点,REALWORLD_JAVA_ANNOTATION_V1 名碰撞 9 FP 根因)
1254
+ const nameCounts = new Map();
1255
+ for (const f of functions) {
1256
+ const n = String(f.name);
1257
+ nameCounts.set(n, (nameCounts.get(n) || 0) + 1);
1258
+ }
1251
1259
  for (const f of functions) {
1252
1260
  if (!f.protocol)
1253
1261
  continue;
1254
1262
  const protocol = { ...f.protocol };
1255
- const existing = protocolRulesData.rules.get(String(f.name));
1263
+ const fname = String(f.name);
1264
+ const existing = protocolRulesData.rules.get(fname);
1256
1265
  if (existing?.namespace && !protocol.namespace) {
1257
1266
  protocol.namespace = existing.namespace;
1258
1267
  }
1259
1268
  // 修复路径渲染真实函数名(fixPath 输出项目原语而非通用规则名)
1260
- protocol.displayName = String(f.name);
1261
- protocolRulesData.rules.set(String(f.name), protocol);
1269
+ protocol.displayName = fname;
1270
+ // Java(className 存在且 name 无点):总是注册 Class.method 限定键;
1271
+ // 裸键仅在项目内唯一时注册(碰撞名只留限定键,匹配走限定调用)
1272
+ const qualified = f.className && !fname.includes(".")
1273
+ ? `${f.className}.${fname}` : undefined;
1274
+ const nameUnique = (nameCounts.get(fname) || 0) === 1;
1275
+ if (qualified) {
1276
+ protocolRulesData.rules.set(qualified, protocol);
1277
+ }
1278
+ if (!f.className || nameUnique) {
1279
+ protocolRulesData.rules.set(fname, protocol);
1280
+ }
1262
1281
  // CamelCase 真实命名(C 代码普遍,如 ACLCheckAllPerm)注册的规则
1263
1282
  // 原样无法被任何匹配策略触达(normalize 只作用于调用名;词段匹配
1264
1283
  // 要求 ≥2 个下划线词段)——同步注册规范化形态使注解原语可被按名命中。
1265
1284
  // 加性改动:snake_case 注解(TS/Python 惯例)normalized === 原名,无变化。
1266
- const normalized = (0, ssg_bridge_1.normalizeName)(String(f.name));
1267
- if (normalized !== String(f.name)) {
1285
+ const normalized = (0, ssg_bridge_1.normalizeName)(fname);
1286
+ if (normalized !== fname && (!f.className || nameUnique)) {
1268
1287
  protocolRulesData.rules.set(normalized, protocol);
1269
1288
  }
1270
1289
  }
@@ -1272,6 +1291,24 @@ async function collectProtocolViolations(ctx, callGraph) {
1272
1291
  }
1273
1292
  catch { /* best-effort */ }
1274
1293
  }
1294
+ // ── 项目别名重校验(注解合并后)──
1295
+ // 注解合并注册的限定键(Java Class.method)在 loadProtocolRules 时还不
1296
+ // 存在,指向它们的别名当时被「规则不存在」校验拒绝——合并后重跑一次,
1297
+ // 把「变量名→限定规则」映射补进 aliasIndex(如 jwtservice.getsubfromtoken
1298
+ // → DefaultJwtService.getSubFromToken;变量名≠类名的文档化桥接通道)
1299
+ if (protocolRulesData) {
1300
+ try {
1301
+ const pa = (0, ssg_bridge_1.loadProjectAliases)(ctx.projectPath, new Set(protocolRulesData.rules.keys()));
1302
+ if (pa) {
1303
+ for (const [callName, ruleName] of Object.entries(pa.aliases)) {
1304
+ if (!protocolRulesData.aliasIndex.has(callName)) {
1305
+ protocolRulesData.aliasIndex.set(callName, ruleName);
1306
+ }
1307
+ }
1308
+ }
1309
+ }
1310
+ catch { /* best-effort */ }
1311
+ }
1275
1312
  // ── Phase 1-5 Semantic Pipeline ──
1276
1313
  // 规则名集合作为展开的保留单元:规则函数不内联(调用名保留给匹配层)
1277
1314
  const callSequences = extractCallSequencesFromProject(ctx.projectPath, ctx.language, protocolRulesData ? new Set(protocolRulesData.rules.keys()) : undefined);
@@ -265,12 +265,16 @@ function inferRuleName(apiName, domain, description, ruleNames, namespace, alias
265
265
  // 仅项目函数适用——真实 C 语料验证中 ReadFile/WriteFile/DeleteFile 经
266
266
  // normalized 撞上 read_file/write_file/delete_file 是 11/24 FP 的主导源,
267
267
  // 而注解桥接(ACLCheckAllPerm → acl_check_all_perm)全是项目函数,不受影响。
268
+ // Java 限定键(Class.method)与限定调用(user.update)大小写不敏感精确
269
+ // 匹配(变量名 user vs 类名 User);带点调用不参与规范化/词段形态匹配——
270
+ // 末段回退会重造名碰撞(article.update 撞 User.update 的裸键/词段)。
271
+ const isDotted = dotIdx >= 0;
268
272
  for (const ruleName of ruleNames) {
269
- if (lowerApi === ruleName) {
273
+ if (lowerApi === ruleName.toLowerCase()) {
270
274
  return ruleName;
271
275
  }
272
276
  }
273
- if (isProjectFn) {
277
+ if (isProjectFn && !(projectFunctions && isDotted)) {
274
278
  for (const ruleName of ruleNames) {
275
279
  if (normalized === ruleName) {
276
280
  return ruleName;
@@ -280,6 +284,8 @@ function inferRuleName(apiName, domain, description, ruleNames, namespace, alias
280
284
  for (const ruleName of ruleNames) {
281
285
  if (projectFunctions && !isProjectFn)
282
286
  continue;
287
+ if (projectFunctions && isDotted)
288
+ continue; // 带点调用只走限定精确匹配(+别名/域提示)
283
289
  const ruleWords = ruleName.split("_");
284
290
  const callWords = normalized.split("_");
285
291
  // Rule must have at least 2 words for this strategy (single-word rules
@@ -113,6 +113,52 @@ function step(api, domain, description = "") {
113
113
  (0, vitest_1.expect)(result.stats.unmatchedCalls).toBe(steps.length);
114
114
  });
115
115
  });
116
+ // ── 接收者限定名匹配(Java 名碰撞根因修复,2026-09-06)──
117
+ (0, vitest_1.describe)("qualified call → rule inference", () => {
118
+ (0, vitest_1.it)("限定调用精确匹配限定规则(大小写不敏感:user.update → User.update)", () => {
119
+ const prev = rules.get("User.update");
120
+ rules.set("User.update", {
121
+ pre_states: ["PASSWORD_HASHED"],
122
+ post_states: ["PASSWORD_UPDATED"],
123
+ namespace: "registration",
124
+ });
125
+ try {
126
+ const result = (0, ssg_bridge_1.validateSequenceWithSSG)([step("user.update", "registration", "Update user data")], rules, nsInit, "test.java", aliasIndex, undefined, new Set(["user.update", "update"]));
127
+ (0, vitest_1.expect)(result.stats.matchedCalls).toBeGreaterThanOrEqual(1);
128
+ }
129
+ finally {
130
+ if (prev)
131
+ rules.set("User.update", prev);
132
+ else
133
+ rules.delete("User.update");
134
+ }
135
+ });
136
+ (0, vitest_1.it)("不同类限定调用不误配(article.update 不命中 User.update 规则)", () => {
137
+ const prev = rules.get("User.update");
138
+ rules.set("User.update", {
139
+ pre_states: ["PASSWORD_HASHED"],
140
+ post_states: ["PASSWORD_UPDATED"],
141
+ namespace: "registration",
142
+ });
143
+ try {
144
+ const result = (0, ssg_bridge_1.validateSequenceWithSSG)([step("article.update", "registration", "Update article content")], rules, nsInit, "test.java", aliasIndex, undefined, new Set(["article.update", "update"]));
145
+ (0, vitest_1.expect)(result.stats.matchedCalls).toBe(0);
146
+ (0, vitest_1.expect)(result.stats.unmatchedCalls).toBe(1);
147
+ }
148
+ finally {
149
+ if (prev)
150
+ rules.set("User.update", prev);
151
+ else
152
+ rules.delete("User.update");
153
+ }
154
+ });
155
+ (0, vitest_1.it)("带点调用不参与词段匹配(helper.verify_hash 不命中 verify_hash)", () => {
156
+ // 无 projectFunctions 时不限制(回退路径旧行为);提供后带点调用
157
+ // 只走限定精确匹配 + 别名——末段词段回退会重造名碰撞
158
+ const result = (0, ssg_bridge_1.validateSequenceWithSSG)([step("helper.verify_hash", "auth", "Verify something")], rules, nsInit, "test.java", aliasIndex, undefined, new Set(["helper.verify_hash", "verify_hash"]));
159
+ (0, vitest_1.expect)(result.stats.matchedCalls).toBe(0);
160
+ });
161
+ });
116
162
  (0, vitest_1.describe)("alias-based matching (exact)", () => {
117
163
  (0, vitest_1.it)("matches bcrypt.compare to verify_hash via alias", () => {
118
164
  (0, vitest_1.expect)(aliasIndex.get("bcrypt.compare")).toBe("verify_hash");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.21",
3
+ "version": "3.7.22",
4
4
  "description": "Progmune — AI Trust Decision Engine. Verify AI-generated code before it reaches production. Outputs APPROVED / NEEDS_REVIEW / BLOCKED with evidence.",
5
5
  "files": [
6
6
  "dist/",