progmune-runtime 3.7.21 → 3.7.23

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.
@@ -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
  });
@@ -1090,6 +1090,7 @@ ${result.code.split("\n").slice(0, 80).join("\n")}` }] };
1090
1090
  degraded: cert.degraded,
1091
1091
  sessionId: cert.sessionId,
1092
1092
  file: cert.file,
1093
+ timestamp: cert.timestamp,
1093
1094
  },
1094
1095
  accountability: acct ? {
1095
1096
  humanEvents: acct.humanEvents,
@@ -69,7 +69,13 @@ catch {
69
69
  }
70
70
  // 3. Load policy (from project config or defaults)
71
71
  const projectDir = process.env.PROGMUNE_PROJECT_DIR || process.cwd();
72
- const { rules, source } = (0, engine_1.loadPolicyConfig)(projectDir, policyPath);
72
+ const { rules, source, configError } = (0, engine_1.loadPolicyConfig)(projectDir, policyPath);
73
+ // 配置解析失败:拒绝静默评估(fail-closed 信号,审计修复 2026-09-06)
74
+ if (configError) {
75
+ console.error(`❌ ${configError}`);
76
+ console.error(` 拒绝在损坏的策略配置下评估——请修复配置后重试。`);
77
+ process.exit(2);
78
+ }
73
79
  // 4. Evaluate policy
74
80
  const ctx = {
75
81
  certificate: {
@@ -83,6 +89,7 @@ const ctx = {
83
89
  degraded: cert.degraded,
84
90
  sessionId: cert.sessionId,
85
91
  file: cert.file,
92
+ timestamp: cert.timestamp,
86
93
  },
87
94
  accountability: acct ? {
88
95
  humanEvents: acct.humanEvents,