progmune-runtime 3.7.69 → 3.7.71

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,76 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.71] — 2026-10-18
4
+
5
+ ### Express 匿名中间件函数体鉴权证据(fr-014 残留归零)
6
+
7
+ **根因**:旧 `extractGlobalMiddleware` 用 `[^)]+` 捕获 `app.use(...)`
8
+ 实参——匿名中间件函数体的鉴权证据(`app.use(\`${prefix}*\`,
9
+ (req,res,next) => { extractBearerToken(...) ... })`)被截断在第一个
10
+ `)` 处(`(req,res,next)` 的右括号),`classifyMiddleware` 永远看不
11
+ 到函数体 ⇒ `hasAnyAuth` 恒 false ⇒ mockoon 管理 API 修复版本
12
+ (CVE-2026-59148 / GHSA-rqx4-3f6q-3x2v)仍残留 1 条
13
+ EXPRESS_NO_AUTH_MIDDLEWARE critical。
14
+
15
+ - **顶层实参解析(parseUseArgs)**:括号/花括号/字符串/注释感知的
16
+ 顶层逗号切分——匿名中间件函数体完整进入 classifyMiddleware,
17
+ 函数体内的 extractBearerToken/timingSafeEqual 等强证据词命中
18
+ auth 分类。边界如实记录:模板串按无嵌套 ${} 处理;正则字面量
19
+ 不特殊处理(与检测器其他文本级判定同口径)
20
+ - **模板串路径归属边界**:动态路径路由(`${prefix}/x`)跳过逐路由
21
+ EXPRESS_ROUTE_MISSING_AUTH 检查——全局守卫挂载路径同样是变量
22
+ 拼接,检测器无法证明「该路由不在守卫覆盖内」,逐路由告警是不
23
+ 可归属的噪声(mockoon 修复形态实测 12 条全 FP)。保守边界:整
24
+ app 无认证的 critical 不受影响(动态路径照常进 hasNakedMutation
25
+ 计数,修复前形态照报 critical)
26
+ - **fr-014 三态复测(mini 语料,真值文件 raw 直链)**:修复前
27
+ admin-api.ts 报 critical + server.ts 3 条加固建议类(两版本一致);
28
+ 修复后 0 critical + 同 3 条建议类——**注册表升级为干净 DETECTED**
29
+ (fr-corpus 有效 11/17 的 with-boundary 项收窄)
30
+ - **回归锁定**:express 检测器 33/33(+6 例:函数体证据/三态/嵌套
31
+ 实参不切碎/字面量路由照报/注释括号);框架全族 188/188;引擎 +
32
+ 检测器族 245/245;express-realworld 20 路由 0 协议级 FP 保持
33
+ (仅已知 NO_HELMET 加固建议边界)
34
+
35
+ ## [3.7.70] — 2026-10-18
36
+
37
+ ### 裸名族兜底传播豁免(修冤枉第十一刀)
38
+
39
+ **根因**:computeResolvedCallerMap 分层消歧(后缀唯一→同文件→同目录
40
+ →类根)失败时挂未解析边 fail-safe ⇒ 豁免被阻断。第九刀探针实测
41
+ C4/C5 残部里「歧义边」占 50/70 条——但歧义≠证据缺失:同名族调用方
42
+ **并集**全落在豁免集合内时,最坏归因(每个族成员都被每个调用方
43
+ 调用)也在并集内,不需要边解析。
44
+
45
+ - **内核(computeBareFamilyPropagatedSet)**:第七刀 B 判据泛化——
46
+ 裸名族调用方并集全 ⊆ base ⇒ 全族豁免;裸化只增候选(R75 正向
47
+ 证据)⇒ 并集全豁免蕴含真实调用方全豁免;比逐边消歧更保守
48
+ (族内任一成员被并集外调用则全族不豁免);快照轮次默认 1;
49
+ computeJobSurfacePropagatedSet 重构委托本内核(includeBase 保留
50
+ 第七刀「含种子」历史语义,11 例既有测试锁零漂移)
51
+ - **引擎接线**:base = 公开∪守卫∪作业∪混合∪公开传播;suppressed 带
52
+ `suppressedByBareFamilyPropagation` 标记(R9),
53
+ `PROGMUNE_BARE_FAMILY_TOGGLE=off` 逃生门;消费顺序
54
+ guard→job→mixed→public→family(未豁免才挂标记)
55
+ - **A/B(docmost,各 fresh IR)**:control(toggle off)与 3.7.69
56
+ 基线**逐条一致**(642/166,diff 0);pre **642→585(−57)**,
57
+ suppressed 166→223(+57 全带 family 标记),主路径 11=11/84=84。
58
+ −57 = 探针预测的 50 条歧义边类 + **7 条「已豁免成员下游一跳」类**
59
+ (如 getGreetingName ← PageUpdateEmail、getPageContext ←
60
+ notifyMentionedUsers——族调用方是第五~十刀豁免成员的函数,内核
61
+ base 含传播集故自然覆盖;逐条核族调用方 100% 在并集内,TP 0)
62
+ - **FN 风险逐条复核**:57 条全核调用方来源——守卫 controller
63
+ (updateSpace/removeSpaceIcon/markAllAsRead 类)、@OnEvent 监听器
64
+ 作业链(isTypesense→getSearchDriver 类)、公开路由、既有豁免成员
65
+ 下游;族并集外调用方 148 条照报(不豁免)
66
+ - **回归锁定**:内核 9 例 + 引擎产品路径 2;全量 552/552;三闸门
67
+ (taintpath 160 / webshape 72 / fr-007 pre5-post0 + fr-016
68
+ pre7-post0);TS 盲测 119 项目 LOST 0/ADDED 0(引擎侧豁免不进
69
+ batch-scan,构造保证 + 实测)
70
+ - **过程教训**:探针只统计「有未解析边」的告警,低估 7 条——
71
+ 传播内核作用于全量函数,「已豁免成员下游」也是合法覆盖。探针
72
+ 过滤器必须与实现判据逐字对齐(R59 家族)
73
+
3
74
  ## [3.7.69] — 2026-10-17
4
75
 
5
76
  ### 公开表面传播豁免(修冤枉第十刀)
@@ -223,23 +223,124 @@ function classifyMiddleware(code, name) {
223
223
  return "logging";
224
224
  return "unknown";
225
225
  }
226
+ /**
227
+ * 解析 app.use(...) 的实参列表(2026-10-18,fr-014 残留根因修复):
228
+ * 顶层逗号切分 + 括号/花括号/字符串/注释感知。
229
+ *
230
+ * 根因:旧实现用 `[^)]+` 捕获实参——匿名中间件函数体的鉴权证据
231
+ * (app.use(`${prefix}*`, (req,res,next) => { extractBearerToken(...)
232
+ * ... }))被截断在第一个 `)` 处((req,res,next) 的右括号),
233
+ * classifyMiddleware 永远看不到函数体 ⇒ hasAnyAuth 恒 false。
234
+ *
235
+ * 边界(如实记录):模板串按无嵌套 ${} 处理(路径字面量形态;
236
+ * ${} 内含反引号/顶层括号的极端形态会错切);正则字面量不特殊
237
+ * 处理(检测器不做语法树,与其他文本级判定同口径)。
238
+ */
239
+ function parseUseArgs(code, openParenIndex) {
240
+ const args = [];
241
+ let i = openParenIndex + 1;
242
+ let paren = 1; // app.use( 的实参层
243
+ let brace = 0;
244
+ let argStart = i;
245
+ let quote = null;
246
+ let lineComment = false;
247
+ let blockComment = false;
248
+ while (i < code.length) {
249
+ const ch = code[i];
250
+ if (lineComment) {
251
+ if (ch === "\n")
252
+ lineComment = false;
253
+ i++;
254
+ continue;
255
+ }
256
+ if (blockComment) {
257
+ if (ch === "*" && code[i + 1] === "/") {
258
+ blockComment = false;
259
+ i += 2;
260
+ continue;
261
+ }
262
+ i++;
263
+ continue;
264
+ }
265
+ if (quote) {
266
+ if (ch === "\\") {
267
+ i += 2;
268
+ continue;
269
+ }
270
+ if (ch === quote)
271
+ quote = null;
272
+ i++;
273
+ continue;
274
+ }
275
+ if (ch === "'" || ch === '"' || ch === "`") {
276
+ quote = ch;
277
+ i++;
278
+ continue;
279
+ }
280
+ if (ch === "/" && code[i + 1] === "/") {
281
+ lineComment = true;
282
+ i += 2;
283
+ continue;
284
+ }
285
+ if (ch === "/" && code[i + 1] === "*") {
286
+ blockComment = true;
287
+ i += 2;
288
+ continue;
289
+ }
290
+ if (ch === "(") {
291
+ paren++;
292
+ i++;
293
+ continue;
294
+ }
295
+ if (ch === ")") {
296
+ paren--;
297
+ if (paren === 0) {
298
+ const text = code.slice(argStart, i).trim();
299
+ if (text)
300
+ args.push({ text, start: argStart });
301
+ return args;
302
+ }
303
+ i++;
304
+ continue;
305
+ }
306
+ if (ch === "{") {
307
+ brace++;
308
+ i++;
309
+ continue;
310
+ }
311
+ if (ch === "}") {
312
+ brace = Math.max(0, brace - 1);
313
+ i++;
314
+ continue;
315
+ }
316
+ if (ch === "," && paren === 1 && brace === 0) {
317
+ const text = code.slice(argStart, i).trim();
318
+ if (text)
319
+ args.push({ text, start: argStart });
320
+ argStart = i + 1;
321
+ }
322
+ i++;
323
+ }
324
+ return args;
325
+ }
226
326
  /**
227
327
  * Extract global middleware registrations (app.use(fn)).
228
328
  */
229
329
  function extractGlobalMiddleware(code, appName) {
230
330
  const middleware = [];
231
- const useRegex = new RegExp(`${appName}\\.use\\s*\\(([^)]+)\\)`, "gi");
331
+ const useRegex = new RegExp(`${appName}\\.use\\s*\\(`, "gi");
232
332
  let match;
233
333
  while ((match = useRegex.exec(code)) !== null) {
234
- const args = match[1].split(",").map(a => a.trim());
235
- for (const arg of args) {
236
- if (arg.startsWith("'") || arg.startsWith('"'))
334
+ const openParen = match.index + match[0].length - 1;
335
+ for (const arg of parseUseArgs(code, openParen)) {
336
+ const t = arg.text;
337
+ if (/^['"`]/.test(t))
237
338
  continue; // skip path strings
238
- if (/^(express|router|Router)\./.test(arg))
339
+ if (/^(express|router|Router)\./.test(t))
239
340
  continue; // skip sub-routers (handled separately)
240
341
  middleware.push({
241
- name: arg,
242
- type: classifyMiddleware(code, arg),
342
+ name: t,
343
+ type: classifyMiddleware(code, t),
243
344
  registeredAt: code.slice(0, match.index).split("\n").length,
244
345
  });
245
346
  }
@@ -311,6 +412,14 @@ function analyzeExpressApp(code) {
311
412
  continue; // 读操作不查(公开读常见)
312
413
  if (routeHasAuth(route))
313
414
  continue;
415
+ // 模板串路径(如 `${adminApiPrefix}/env-vars`)不可静态归属——
416
+ // 全局守卫挂载路径同样是变量拼接(app.use(`${prefix}*`, auth)),
417
+ // 检测器无法证明「该路由不在守卫覆盖内」,逐路由告警是不可归属的
418
+ // 噪声(fr-014 mockoon 修复形态实测 12 条全 FP)。保守边界:动态
419
+ // 路径跳过逐路由检查;整 app 无认证的 critical 检查不受影响
420
+ // (动态路径照常进入 hasNakedMutation 计数)。
421
+ if (route.path.includes("${"))
422
+ continue;
314
423
  if (isPublicRoute(route.path))
315
424
  continue;
316
425
  if (route.method === "post" && (0, route_window_1.isRegisterRoot)(route.path, registerRoots))
@@ -273,3 +273,93 @@ module.exports = router;
273
273
  (0, vitest_1.expect)(issues.some((i) => i.rule === "EXPRESS_ROUTE_MISSING_AUTH" && i.route === "PUT /admin")).toBe(false);
274
274
  });
275
275
  });
276
+ // ═══════════════════════════════════════════════════════════════
277
+ // fr-014 残留修复(2026-10-18):匿名中间件函数体鉴权证据 + 模板
278
+ // 串路径归属边界(mockoon admin-api 修复形态)
279
+ //
280
+ // 根因:extractGlobalMiddleware 旧实现 [^)]+ 把 app.use(`${prefix}*`,
281
+ // (req,res,next) => { ...extractBearerToken... }) 截断在第一个 `)`,
282
+ // 函数体证据永远不可见 ⇒ hasAnyAuth 恒 false ⇒ 修复后仍报 critical。
283
+ // 修复:顶层实参解析(括号/花括号/字符串感知)+ 模板串路径跳过
284
+ // 逐路由检查(动态路径不可静态归属,12 条实测全 FP)。
285
+ // ═══════════════════════════════════════════════════════════════
286
+ (0, vitest_1.describe)("express-detector:匿名中间件函数体鉴权证据(fr-014 残留)", () => {
287
+ const ADMIN_API_FIX_SHAPE = `
288
+ const express = require('express');
289
+ const { timingSafeEqual } = require('node:crypto');
290
+ const setupAdminApi = (app: Express) => {
291
+ const adminApiPrefix = '/mockoon-admin';
292
+ const hasValidAdminApiToken = (token: string) => timingSafeEqual(Buffer.from('x'), Buffer.from(token));
293
+
294
+ app.use(\`\${adminApiPrefix}*\`, (req, res, next) => {
295
+ const bearerToken = extractBearerToken(req.get('authorization'));
296
+ if (!bearerToken || !hasValidAdminApiToken(bearerToken)) {
297
+ res.status(401).send({ message: 'Unauthorized' });
298
+ return;
299
+ }
300
+ next();
301
+ });
302
+
303
+ app.post(\`\${adminApiPrefix}/env-vars\`, setEnvVarHandler);
304
+ app.put(\`\${adminApiPrefix}/env-vars\`, setEnvVarHandler);
305
+ };
306
+
307
+ export { setupAdminApi };
308
+ `;
309
+ const ADMIN_API_PRE_SHAPE = ADMIN_API_FIX_SHAPE.replace(` app.use(\`\${adminApiPrefix}*\`, (req, res, next) => {
310
+ const bearerToken = extractBearerToken(req.get('authorization'));
311
+ if (!bearerToken || !hasValidAdminApiToken(bearerToken)) {
312
+ res.status(401).send({ message: 'Unauthorized' });
313
+ return;
314
+ }
315
+ next();
316
+ });
317
+
318
+ `, "");
319
+ (0, vitest_1.it)("匿名中间件函数体内的 bearer 证据 ⇒ 分类为 auth(修复形态)", () => {
320
+ const mw = (0, express_detector_1.extractGlobalMiddleware)(ADMIN_API_FIX_SHAPE, "app");
321
+ const auth = mw.find((m) => m.type === "auth");
322
+ (0, vitest_1.expect)(auth).toBeDefined();
323
+ (0, vitest_1.expect)(auth.name).toContain("extractBearerToken");
324
+ });
325
+ (0, vitest_1.it)("修复形态(全局匿名 auth + 模板串 mutation 路由)⇒ 0 issues", () => {
326
+ const { issues } = (0, express_detector_1.analyzeExpressApp)(ADMIN_API_FIX_SHAPE);
327
+ (0, vitest_1.expect)(issues).toEqual([]);
328
+ });
329
+ (0, vitest_1.it)("修复前形态(无任何鉴权证据 + 模板串 mutation 路由)⇒ critical 照报", () => {
330
+ const { issues } = (0, express_detector_1.analyzeExpressApp)(ADMIN_API_PRE_SHAPE);
331
+ (0, vitest_1.expect)(issues.some((i) => i.rule === "EXPRESS_NO_AUTH_MIDDLEWARE")).toBe(true);
332
+ });
333
+ (0, vitest_1.it)("嵌套调用实参不被切碎(app.use(cors({origin:'x,y'})) 分类 cors)", () => {
334
+ const code = `
335
+ const app = require('express')();
336
+ app.use(cors({ origin: 'a,b', methods: ['GET','POST'] }));
337
+ app.post('/x', handler);
338
+ `;
339
+ const mw = (0, express_detector_1.extractGlobalMiddleware)(code, "app");
340
+ (0, vitest_1.expect)(mw.some((m) => m.type === "cors")).toBe(true);
341
+ // 逗号在字符串/嵌套括号内不产生碎实参
342
+ (0, vitest_1.expect)(mw.length).toBe(1);
343
+ });
344
+ (0, vitest_1.it)("字面量路径 mutation 路由在 hasAnyAuth 下仍逐路由报(回归锁定)", () => {
345
+ const code = `
346
+ const app = require('express')();
347
+ app.use(passport.initialize());
348
+ app.post('/admin/secret', handler);
349
+ `;
350
+ const { issues } = (0, express_detector_1.analyzeExpressApp)(code);
351
+ (0, vitest_1.expect)(issues.some((i) => i.rule === "EXPRESS_ROUTE_MISSING_AUTH" && i.route === "POST /admin/secret")).toBe(true);
352
+ });
353
+ (0, vitest_1.it)("注释内的括号/花括号不破坏实参解析", () => {
354
+ const code = `
355
+ const app = require('express')();
356
+ app.use(
357
+ // (注释里的括号) 与 /* 块注释 { } */
358
+ (req, res, next) => { /* } */ if (req.url) { next(); } }
359
+ );
360
+ app.post('/x', handler);
361
+ `;
362
+ const mw = (0, express_detector_1.extractGlobalMiddleware)(code, "app");
363
+ (0, vitest_1.expect)(mw.length).toBe(1);
364
+ });
365
+ });
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const protocol_detector_1 = require("./protocol-detector");
5
+ /**
6
+ * 裸名族兜底传播内核(修冤枉第十一刀,2026-10-17)单元测试。
7
+ *
8
+ * B 判据(见 computeBareFamilyPropagatedSet 注释):
9
+ * 裸名族调用方并集全 ⊆ base ⇒ 族内每个成员豁免。
10
+ * 正确性(R75 正向证据):裸化只增候选 ⇒ 真实调用方必在并集中 ⇒
11
+ * 并集全豁免蕴含真实调用方全豁免——不需要边解析。
12
+ * 比逐边消歧更保守:族内任一成员被并集外调用则全族不豁免。
13
+ * 第七刀经 includeBase=true 与本内核共享(既有 11 例锁定)。
14
+ */
15
+ (0, vitest_1.describe)("computeBareFamilyPropagatedSet(裸名族兜底,B 判据内核)", () => {
16
+ (0, vitest_1.it)("歧义族(同名两候选)+ 调用方并集全在 base ⇒ 全族豁免(逐边消歧失败的形态)", () => {
17
+ // 第五/九/十刀的逐边消歧在此形态挂未解析边 fail-safe;
18
+ // 族级兜底不需要边解析——最坏归因也落在并集内。
19
+ const fns = [
20
+ { name: "GuardedController.get", file: "c/page.controller.ts", calls: ["getData"] },
21
+ { name: "OtherGuardedController.get", file: "c/space.controller.ts", calls: ["getData"] },
22
+ { name: "PageService.getData", file: "s/page.service.ts", calls: [] },
23
+ { name: "SpaceService.getData", file: "s/space.service.ts", calls: [] },
24
+ ];
25
+ const base = new Set(["GuardedController.get", "OtherGuardedController.get"]);
26
+ const g = (0, protocol_detector_1.computeBareFamilyPropagatedSet)(fns, base);
27
+ (0, vitest_1.expect)(g.has("PageService.getData")).toBe(true);
28
+ (0, vitest_1.expect)(g.has("SpaceService.getData")).toBe(true);
29
+ });
30
+ (0, vitest_1.it)("族内任一成员被并集外调用 ⇒ 全族不豁免(族级保守)", () => {
31
+ const fns = [
32
+ { name: "GuardedController.get", file: "c1.ts", calls: ["getData"] },
33
+ { name: "PublicController.get", file: "c2.ts", calls: ["getData"] }, // 并集外
34
+ { name: "PageService.getData", file: "s1.ts", calls: [] },
35
+ { name: "SpaceService.getData", file: "s2.ts", calls: [] },
36
+ ];
37
+ const base = new Set(["GuardedController.get"]);
38
+ const g = (0, protocol_detector_1.computeBareFamilyPropagatedSet)(fns, base);
39
+ (0, vitest_1.expect)(g.has("PageService.getData")).toBe(false);
40
+ (0, vitest_1.expect)(g.has("SpaceService.getData")).toBe(false);
41
+ });
42
+ (0, vitest_1.it)("无调用方(入口/未连通)⇒ 不豁免", () => {
43
+ const fns = [{ name: "orphanEntry", file: "m.ts", calls: [] }];
44
+ const g = (0, protocol_detector_1.computeBareFamilyPropagatedSet)(fns, new Set());
45
+ (0, vitest_1.expect)(g.has("orphanEntry")).toBe(false);
46
+ });
47
+ (0, vitest_1.it)("默认 1 轮:族豁免成员的下游不豁免(传递闭包未开)", () => {
48
+ const fns = [
49
+ { name: "GuardedController.get", file: "c.ts", calls: ["getData"] },
50
+ { name: "PageService.getData", file: "s.ts", calls: ["loadMore"] },
51
+ { name: "loadMore", file: "s.ts", calls: [] },
52
+ ];
53
+ const g = (0, protocol_detector_1.computeBareFamilyPropagatedSet)(fns, new Set(["GuardedController.get"]));
54
+ (0, vitest_1.expect)(g.has("PageService.getData")).toBe(true);
55
+ (0, vitest_1.expect)(g.has("loadMore")).toBe(false);
56
+ });
57
+ (0, vitest_1.it)("maxRounds=2 ⇒ 传递两跳成立(快照轮次语义)", () => {
58
+ const fns = [
59
+ { name: "GuardedController.get", file: "c.ts", calls: ["getData"] },
60
+ { name: "PageService.getData", file: "s.ts", calls: ["loadMore"] },
61
+ { name: "loadMore", file: "s.ts", calls: [] },
62
+ ];
63
+ const g = (0, protocol_detector_1.computeBareFamilyPropagatedSet)(fns, new Set(["GuardedController.get"]), { maxRounds: 2 });
64
+ (0, vitest_1.expect)(g.has("PageService.getData")).toBe(true);
65
+ (0, vitest_1.expect)(g.has("loadMore")).toBe(true);
66
+ });
67
+ (0, vitest_1.it)("默认返回只含 base 新增成员(R5);includeBase=true 返回全集(第七刀语义)", () => {
68
+ const fns = [
69
+ { name: "GuardedController.get", file: "c.ts", calls: ["getData"] },
70
+ { name: "PageService.getData", file: "s.ts", calls: [] },
71
+ ];
72
+ const base = new Set(["GuardedController.get"]);
73
+ const added = (0, protocol_detector_1.computeBareFamilyPropagatedSet)(fns, base);
74
+ (0, vitest_1.expect)(added.has("GuardedController.get")).toBe(false); // base 不重复
75
+ (0, vitest_1.expect)(added.has("PageService.getData")).toBe(true);
76
+ const full = (0, protocol_detector_1.computeBareFamilyPropagatedSet)(fns, base, { includeBase: true });
77
+ (0, vitest_1.expect)(full.has("GuardedController.get")).toBe(true);
78
+ (0, vitest_1.expect)(full.has("PageService.getData")).toBe(true);
79
+ });
80
+ (0, vitest_1.it)("自调不提供豁免证据", () => {
81
+ const fns = [{ name: "CleanupService.cleanup", file: "s.ts", calls: ["cleanup"] }];
82
+ const g = (0, protocol_detector_1.computeBareFamilyPropagatedSet)(fns, new Set());
83
+ (0, vitest_1.expect)(g.has("CleanupService.cleanup")).toBe(false);
84
+ });
85
+ (0, vitest_1.it)("限定形态调用名(Class.method)按裸名并入并集(裸化只增候选,保守方向)", () => {
86
+ const fns = [
87
+ { name: "GuardedController.get", file: "c.ts", calls: ["PageService.getData"] },
88
+ { name: "PageService.getData", file: "s.ts", calls: [] },
89
+ ];
90
+ const g = (0, protocol_detector_1.computeBareFamilyPropagatedSet)(fns, new Set(["GuardedController.get"]));
91
+ (0, vitest_1.expect)(g.has("PageService.getData")).toBe(true);
92
+ });
93
+ (0, vitest_1.it)("空 base ⇒ 无人豁免(无正向证据不豁免,R75)", () => {
94
+ const fns = [
95
+ { name: "A", file: "a.ts", calls: ["b"] },
96
+ { name: "b", file: "a.ts", calls: [] },
97
+ ];
98
+ const g = (0, protocol_detector_1.computeBareFamilyPropagatedSet)(fns, new Set());
99
+ (0, vitest_1.expect)(g.has("b")).toBe(false);
100
+ });
101
+ });
@@ -17,6 +17,7 @@ exports.buildCallerMap = buildCallerMap;
17
17
  exports.computeResolvedCallerMap = computeResolvedCallerMap;
18
18
  exports.computeGuardPropagatedSet = computeGuardPropagatedSet;
19
19
  exports.computeJobSurfacePropagatedSet = computeJobSurfacePropagatedSet;
20
+ exports.computeBareFamilyPropagatedSet = computeBareFamilyPropagatedSet;
20
21
  exports.computeMixedSurfacePropagatedSet = computeMixedSurfacePropagatedSet;
21
22
  exports.computePublicSurfacePropagatedSet = computePublicSurfacePropagatedSet;
22
23
  exports.detectSafeguardViolationsV7 = detectSafeguardViolationsV7;
@@ -1798,6 +1799,27 @@ function computeGuardPropagatedSet(funcs, opts) {
1798
1799
  function computeJobSurfacePropagatedSet(funcs, opts) {
1799
1800
  const marker = opts?.marker ?? "__progmune_no_request_surface__";
1800
1801
  const maxHops = opts?.maxHops ?? 1;
1802
+ // 种子:直接带标记的函数(含种子的返回语义由消费方 R6 过滤)
1803
+ const seeds = new Set();
1804
+ for (const f of funcs)
1805
+ if ((f.calls || []).includes(marker))
1806
+ seeds.add(f.name);
1807
+ return computeBareFamilyPropagatedSet(funcs, seeds, { maxRounds: maxHops, includeBase: true });
1808
+ }
1809
+ /**
1810
+ * 裸名族兜底传播内核(B 判据):裸名族调用方并集全 ⊆ base ⇒ 族内
1811
+ * 每个成员豁免。正确性(R75 正向证据):裸化只增候选 ⇒ 真实调用方
1812
+ * 必在并集中 ⇒ 并集全豁免蕴含真实调用方全豁免——**不需要边解析**,
1813
+ * 即使最坏归因(每个族成员都被每个调用方调用)也落在并集内。
1814
+ * 比逐边消歧更保守:族内任一成员被并集外调用则全族不豁免。
1815
+ *
1816
+ * 第七刀(computeJobSurfacePropagatedSet)与第十一刀共用本内核;
1817
+ * 默认返回只含 base 新增成员(includeBase=false),第七刀保留
1818
+ * 「含种子」的历史语义(消费方 R6 过滤)故传 includeBase=true。
1819
+ * 快照轮次语义;无调用方的函数不豁免。
1820
+ */
1821
+ function computeBareFamilyPropagatedSet(funcs, base, opts) {
1822
+ const maxRounds = opts?.maxRounds ?? 1;
1801
1823
  const bySuffix = new Map();
1802
1824
  for (const f of funcs) {
1803
1825
  const bare = f.name.split(".").pop() || f.name;
@@ -1816,11 +1838,9 @@ function computeJobSurfacePropagatedSet(funcs, opts) {
1816
1838
  callersByBare.get(bareC).add(f.name);
1817
1839
  }
1818
1840
  }
1819
- const exempt = new Set();
1820
- for (const f of funcs)
1821
- if ((f.calls || []).includes(marker))
1822
- exempt.add(f.name);
1823
- for (let round = 0; round < maxHops; round++) {
1841
+ const exempt = new Set(base);
1842
+ const added = new Set();
1843
+ for (let round = 0; round < maxRounds; round++) {
1824
1844
  const toAdd = [];
1825
1845
  for (const f of funcs) {
1826
1846
  if (exempt.has(f.name))
@@ -1834,10 +1854,12 @@ function computeJobSurfacePropagatedSet(funcs, opts) {
1834
1854
  }
1835
1855
  if (toAdd.length === 0)
1836
1856
  break;
1837
- for (const n of toAdd)
1857
+ for (const n of toAdd) {
1838
1858
  exempt.add(n);
1859
+ added.add(n);
1860
+ }
1839
1861
  }
1840
- return exempt;
1862
+ return opts?.includeBase ? exempt : added;
1841
1863
  }
1842
1864
  /**
1843
1865
  * 修冤枉第九刀(2026-10-16):调用方混合并集豁免——
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.69";
23
+ exports.RUNTIME_VERSION = "3.7.71";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
@@ -1535,6 +1535,21 @@ async function collectProtocolViolations(ctx, callGraph) {
1535
1535
  const publicSurfacePropagated = process.env.PROGMUNE_PUBLIC_SURFACE_TOGGLE === "off"
1536
1536
  ? new Set()
1537
1537
  : (0, protocol_detector_1.computePublicSurfacePropagatedSet)(functions, publicMarked, guardPropagated, jobSurfacePropagated);
1538
+ // 修冤枉第十一刀(2026-10-17):裸名族兜底传播——逐边消歧失败
1539
+ // 的函数用 B 判据族级保守形态兜底:裸名族调用方并集全 ⊆ base
1540
+ // (公开∪守卫∪作业∪既有传播)⇒ 全族豁免。正确性:裸化只增候选
1541
+ // ⇒ 真实调用方必在并集中(不需要边解析,最坏归因也在并集内)。
1542
+ // 比逐边消歧更保守:族内任一成员被并集外调用则全族不豁免。
1543
+ // 默认 1 轮;豁免仅 authorization 类且不作用于直接标记函数。
1544
+ const bareFamilyPropagated = process.env.PROGMUNE_BARE_FAMILY_TOGGLE === "off"
1545
+ ? new Set()
1546
+ : (0, protocol_detector_1.computeBareFamilyPropagatedSet)(functions, new Set([
1547
+ ...publicMarked,
1548
+ ...guardPropagated,
1549
+ ...jobSurfacePropagated,
1550
+ ...mixedSurfacePropagated,
1551
+ ...publicSurfacePropagated,
1552
+ ]));
1538
1553
  for (const f of functions) {
1539
1554
  const fCalls = f.calls || [];
1540
1555
  let svs;
@@ -1554,6 +1569,7 @@ async function collectProtocolViolations(ctx, callGraph) {
1554
1569
  const jobExempt = !directlyJobMarked && jobSurfacePropagated.has(f.name);
1555
1570
  const mixedExempt = !directlyMarked && !directlyJobMarked && mixedSurfacePropagated.has(f.name);
1556
1571
  const publicExempt = !directlyPublicMarked && publicSurfacePropagated.has(f.name);
1572
+ const familyExempt = !directlyMarked && !directlyJobMarked && !directlyPublicMarked && bareFamilyPropagated.has(f.name);
1557
1573
  for (const v of svs) {
1558
1574
  const alert = {
1559
1575
  rule: v.rule,
@@ -1581,6 +1597,10 @@ async function collectProtocolViolations(ctx, callGraph) {
1581
1597
  alert.suppressedByPublicSurfacePropagation = true;
1582
1598
  suppressed = true;
1583
1599
  }
1600
+ if (!suppressed && familyExempt) {
1601
+ alert.suppressedByBareFamilyPropagation = true;
1602
+ suppressed = true;
1603
+ }
1584
1604
  if (suppressed) {
1585
1605
  safeguardAlertsSuppressed.push(alert);
1586
1606
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.69",
3
+ "version": "3.7.71",
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/",