progmune-runtime 3.7.67 → 3.7.69

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,91 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.69] — 2026-10-17
4
+
5
+ ### 公开表面传播豁免(修冤枉第十刀)
6
+
7
+ **根因**:@Public()/@SkipAuth()/@AllowAnonymous 是开发者**显式声明**
8
+ 免鉴权(正向证据,R75),但提取器只在 E2 的 skip 方向认识它们
9
+ (「不注入鉴权标记」)——授权族规则照旧提问「缺鉴权」,公开端点
10
+ 自身(docmost 15 个 @Public 方法 17 条告警)与纯公开下游(10 条)
11
+ 全数误报。与第六刀(无请求面)同族,差在面向请求面——公开端点
12
+ 是请求面,只是免鉴权。
13
+
14
+ - **提取器**:类级/方法级 @Public/@SkipAuth/@AllowAnonymous ⇒ 注入
15
+ `__progmune_public_surface__`(与 E2/第六刀并列;类级作用于全方法)。
16
+ ⚠ 注入名只收三个 NestJS 惯例装饰器,与 E2 的 AUTH_SKIP_DECORATOR
17
+ **不对称是故意的**:skip 方向多收(IsPublic/NoAuth/ApiExcludeEndpoint)
18
+ 是防「公开判成有鉴权」,注入方向少收是防「受限判成公开」——两个
19
+ 方向的错判都是假阴性;@ApiExcludeEndpoint 是 Swagger 文档语义,
20
+ 绝不注入
21
+ - **规则侧(Part A,第六刀同架构)**:授权族四条(Access/Mutation/
22
+ Ownership/Resource Ownership)safeguard 接受标记——公开端点自身的
23
+ 告警直接消除(标记本身就是证据,与 no_request_surface 并列)
24
+ - **传播(Part B)**:computePublicSurfacePropagatedSet——F 的每个
25
+ 直接调用方 ∈ {公开面} ∪ {已守卫} ∪ {作业面} ⇒ 豁免;R1 调用方非空
26
+ / R2 边全解析 / R3 并集外照报 / R4 默认 1 轮 / R5 返回只含新增成员。
27
+ 引擎侧 suppressed 带 `suppressedByPublicSurfacePropagation` 标记(R9),
28
+ `PROGMUNE_PUBLIC_SURFACE_TOGGLE=off` 逃生门;并集含守卫∪作业 ⇒ 是
29
+ 第九刀超集,消费顺序 guard→job→mixed→public 保证不重复挂标记
30
+ - **三向 A/B(docmost,各 fresh IR)**:①control(传播关)vs 3.7.68
31
+ 基线 = **精确 −17 条 Part A**(全落 12 个 @Public 方法,新增 0,
32
+ suppressed 156=156 不变);②pre vs control = **精确 −10 条 Part B**
33
+ (全带 public 标记,suppressed 156→166);③主路径 violations
34
+ 11=11 / score 84=84 三臂一致。告警流累计 992→…→685→669→**642**
35
+ - **FN 风险逐条复核**:10 条传播候选全核调用方——搜索/分享/公开空间
36
+ 服务只被 @UseGuards(JwtAuthGuard) 类级守卫路由或 @Public 路由调用
37
+ (search.controller.ts 类级守卫 + 方法级 @Public 并存形态),TP 0;
38
+ 公开端点自身由装饰器显式声明
39
+ - **回归锁定**:提取器 5 例 + 规则侧 5 例 + 传播 10 例 + 引擎产品
40
+ 路径 2;全量 541/541(首跑 1 失败为 worker 超时环境噪音,复跑全绿);
41
+ 三闸门(taintpath 160 / webshape 72 / fr-007 pre5-post0 + fr-016
42
+ pre7-post0);TS 盲测 119 项目 LOST 0/ADDED 0(generated 语料无
43
+ @Public 形状,零漂移由构造保证 R23 家族)
44
+
45
+ ## [3.7.68] — 2026-10-16
46
+
47
+ ### 调用方混合并集豁免(修冤枉第九刀)
48
+
49
+ **根因**:第五刀(全部调用方已守卫)与第七刀(全部调用方是作业)
50
+ 各留下边界:① 守卫传播 ≤3 跳,第 4 跳的服务层函数(如
51
+ ExportService.getWorkspaceBaseUrl)在守卫集外;② 第七刀 B 判据是
52
+ **裸名族级**保守——同名族任一成员被 HTTP 面调用则全族不豁免,
53
+ 族内「只被作业调用」的成员(如 VerificationNotificationService.
54
+ getPageContext,同为 getPageContext 裸名的 Page 族成员被 controller
55
+ 调用)被连坐照报。两者语义上都没有未鉴权请求面:每条到达 F 的
56
+ 路径要么已过守卫、要么源起作业处理器。
57
+
58
+ - **判据(computeMixedSurfacePropagatedSet)**:函数 F 的每个直接
59
+ 调用方都 ∈ {已守卫传播集} ∪ {作业面传播集} ⇒ 豁免。保守规则:
60
+ R1 调用方集合非空(HTTP 入口无项目调用方,永不豁免);R2 每条
61
+ 调用边必须可解析(与第五刀共享 computeResolvedCallerMap 分层消歧,
62
+ 歧义 fail-safe);R3 任一调用方在并集外 → 照报;R4 快照轮次语义,
63
+ 默认 1 轮(传递闭包留数据驱动);R5 返回集合只含并集新增成员
64
+ - **引擎接线**:豁免仅 authorization 类、函数自身不直接带标记
65
+ (R5/R6);被压告警进 `safeguardAlerts.suppressed` 带
66
+ `suppressedByMixedSurfacePropagation` 标记(标记而非丢弃,R9);
67
+ `PROGMUNE_MIXED_SURFACE_TOGGLE=off` 逃生门
68
+ - **同条件 A/B(docmost,各 fresh IR)**:主告警流 685→669
69
+ (−16 = Access 13 + Mutation 2 + Ownership 1,消失清单与离线重放
70
+ 预测逐条一致、新增 0);**control(toggle off)685/140 与 3.7.67
71
+ 基线逐条一致**;主路径 violations 11=11 / score 84=84 /
72
+ NEEDS_REVIEW 不变;suppressed 140→156(156=117 守卫 +23 作业
73
+ +16 混合,669+156=825 证据守恒)
74
+ - **FN 风险逐条复核**:16 条全核调用方来源——3 条守卫类
75
+ (@UseGuards(JwtAuthGuard) 导出路由 → export 工具函数,守卫传播
76
+ 第 4 跳);13 条作业类(@Processor 队列 → 通知/历史服务方法 →
77
+ 下游 getter/写入,B 判据族级连坐的成员)。TP 0
78
+ - **C4/C5 主力类探针结论(knife9-mixed-surface-probe)**:
79
+ repo-update 类 137 / getter 类 213 中,无调用方(HTTP 入口)62/92、
80
+ 歧义边 50/70、并集外调用方 23/42——「公开设计端点/可达但非
81
+ 鉴权敏感」是语义问题,调用方面传播已到边界(守卫深度敏感性:
82
+ maxDepth 3→6 仅 +6 条)
83
+ - **回归锁定**:单测 11 例(protocol-detector-mixed-surface.test.ts,
84
+ 含与第七刀族级保守的差异用例)+ 引擎产品路径 2 + 检测器族
85
+ 199/199 + 引擎 239/239;三闸门(taintpath 160 / webshape 72 /
86
+ fr-007 pre5-post0 + fr-016 pre7-post0);**TS 盲测 119 项目对
87
+ 3.7.67 提交基线 LOST 0 / ADDED 0**
88
+
3
89
  ## [3.7.67] — 2026-10-12
4
90
 
5
91
  ### 词撞类豁免(修冤枉第八刀:mutation/access trigger 词段碰撞)
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const extract_ir_1 = require("./extract-ir");
41
+ /**
42
+ * 修冤枉第十刀(2026-10-16):公开设计端点 → 公开面标记锁定。
43
+ *
44
+ * 数据:docmost 15 个 @Public() 方法自身 17 条授权族告警全 FP——
45
+ * 公开是开发者显式声明(正向证据,R75),授权族「缺鉴权」提问不适用。
46
+ *
47
+ * 注入形态:类级/方法级 @Public/@SkipAuth/@AllowAnonymous(NestJS 惯例)。
48
+ * 反例(不注入):@ApiExcludeEndpoint 是 Swagger 文档语义;
49
+ * 与 AUTH_SKIP_DECORATOR 不对称是故意的(两个方向的错判都是假阴性)。
50
+ */
51
+ const MARK = "__progmune_public_surface__";
52
+ const TSCONFIG = JSON.stringify({
53
+ compilerOptions: { target: "ES2020", module: "commonjs", strict: false, skipLibCheck: true, noEmit: true },
54
+ include: ["**/*.ts"],
55
+ });
56
+ function makeProject(files) {
57
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-publicsurface-"));
58
+ fs.writeFileSync(path.join(dir, "tsconfig.json"), TSCONFIG);
59
+ fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ name: "fixture", version: "0.0.0", private: true }));
60
+ for (const [rel, body] of Object.entries(files)) {
61
+ const abs = path.join(dir, rel);
62
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
63
+ fs.writeFileSync(abs, body);
64
+ }
65
+ return dir;
66
+ }
67
+ function callsOf(dir, fnName) {
68
+ const fns = (0, extract_ir_1.extractIR)(dir);
69
+ const f = fns.find((x) => x.name === fnName);
70
+ return f ? (f.calls ?? []) : [];
71
+ }
72
+ (0, vitest_1.describe)("extractIR:公开面标记(修冤枉第十刀)", () => {
73
+ (0, vitest_1.it)("@Public() 类级 ⇒ 全部方法注入", () => {
74
+ const dir = makeProject({
75
+ "a.ts": `
76
+ declare const Public: any; declare const Controller: any; declare const Get: any;
77
+ @Public()
78
+ @Controller("public-space")
79
+ export class PublicSpaceController {
80
+ @Get("info")
81
+ getInfo() { return 1; }
82
+ getTree() { return 2; }
83
+ }
84
+ `,
85
+ });
86
+ (0, vitest_1.expect)(callsOf(dir, "PublicSpaceController.getInfo")).toContain(MARK);
87
+ (0, vitest_1.expect)(callsOf(dir, "PublicSpaceController.getTree")).toContain(MARK);
88
+ });
89
+ (0, vitest_1.it)("@Public/@SkipAuth/@AllowAnonymous 方法级 ⇒ 仅该方法注入", () => {
90
+ const dir = makeProject({
91
+ "a.ts": `
92
+ declare const Public: any; declare const SkipAuth: any; declare const AllowAnonymous: any;
93
+ export class ShareController {
94
+ @Public()
95
+ getShare() {}
96
+ @SkipAuth()
97
+ getSharedPageInfo() {}
98
+ @AllowAnonymous()
99
+ transclusionLookup() {}
100
+ getPrivateShare() {}
101
+ }
102
+ `,
103
+ });
104
+ for (const fn of ["ShareController.getShare", "ShareController.getSharedPageInfo", "ShareController.transclusionLookup"]) {
105
+ (0, vitest_1.expect)(callsOf(dir, fn)).toContain(MARK);
106
+ }
107
+ (0, vitest_1.expect)(callsOf(dir, "ShareController.getPrivateShare")).not.toContain(MARK);
108
+ });
109
+ (0, vitest_1.it)("反例:@ApiExcludeEndpoint 是 Swagger 文档语义,不注入", () => {
110
+ const dir = makeProject({
111
+ "a.ts": `
112
+ declare const ApiExcludeEndpoint: any;
113
+ export class DocController {
114
+ @ApiExcludeEndpoint()
115
+ hidden() {}
116
+ }
117
+ `,
118
+ });
119
+ (0, vitest_1.expect)(callsOf(dir, "DocController.hidden")).not.toContain(MARK);
120
+ });
121
+ (0, vitest_1.it)("@UseGuards + @Public 并存 ⇒ public 注入、auth 不注入(显式公开优先)", () => {
122
+ const dir = makeProject({
123
+ "a.ts": `
124
+ declare const UseGuards: any; declare const Public: any;
125
+ declare class JwtAuthGuard {}
126
+ export class C {
127
+ @UseGuards(JwtAuthGuard)
128
+ @Public()
129
+ m() { return 1; }
130
+ }
131
+ `,
132
+ });
133
+ const calls = callsOf(dir, "C.m");
134
+ (0, vitest_1.expect)(calls).toContain(MARK);
135
+ (0, vitest_1.expect)(calls).not.toContain("__progmune_auth_machinery__");
136
+ });
137
+ (0, vitest_1.it)("普通守卫方法 ⇒ 不注入 public、注入 auth(两条通道互不污染)", () => {
138
+ const dir = makeProject({
139
+ "a.ts": `
140
+ declare const UseGuards: any;
141
+ declare class JwtAuthGuard {}
142
+ export class C {
143
+ @UseGuards(JwtAuthGuard)
144
+ m() { return 1; }
145
+ }
146
+ `,
147
+ });
148
+ const calls = callsOf(dir, "C.m");
149
+ (0, vitest_1.expect)(calls).not.toContain(MARK);
150
+ (0, vitest_1.expect)(calls).toContain("__progmune_auth_machinery__");
151
+ });
152
+ });
@@ -500,6 +500,45 @@ function noRequestSurfaceFromDecorators(decorators) {
500
500
  return noSurface ? [NO_REQUEST_SURFACE_MARKER] : [];
501
501
  }
502
502
  // ═══════════════════════════════════════════════════════════════
503
+ // 修冤枉第十刀(2026-10-16):公开设计端点 → 公开面标记
504
+ //
505
+ // 数据:docmost 15 个 @Public() 方法自身 17 条授权族告警 + 下游
506
+ // 纯公开可达 10 条(knife10-public-surface-probe.ts 量化)——公开是
507
+ // 开发者显式声明(正向证据,R75),授权族「缺鉴权」提问不适用。
508
+ //
509
+ // 语义与 E2(auth_machinery)相反:E2 是「有鉴权」,本标记是
510
+ // 「鉴权问题不适用」(与第六刀 no_request_surface 同族,但面向
511
+ // 请求面——公开端点是请求面,只是免鉴权)。
512
+ //
513
+ // 注入名只收三个 NestJS 惯例装饰器:Public / SkipAuth / AllowAnonymous。
514
+ // 与 AUTH_SKIP_DECORATOR(E2 的 skip 方向)不对称是**故意的**:
515
+ // skip 方向多收(IsPublic/NoAuth/ApiExcludeEndpoint)是防「把公开
516
+ // 端点判成有鉴权」的假阴性;注入方向少收是防「把受限端点判成
517
+ // 公开」的假阴性——两个方向的错判都是假阴性,都必须保守。
518
+ // @ApiExcludeEndpoint 是 Swagger 文档语义,绝不注入。
519
+ // ═══════════════════════════════════════════════════════════════
520
+ const PUBLIC_SURFACE_MARKER = "__progmune_public_surface__";
521
+ /** 显式公开装饰器 —— 出现即注入公开面标记(NestJS 惯例) */
522
+ const PUBLIC_SURFACE_DECORATOR = /^(?:Public|SkipAuth|AllowAnonymous)$/i;
523
+ /**
524
+ * 由类/方法装饰器判定是否注入公开面标记(与 E2/第六刀同款文本匹配)。
525
+ * @returns [] 或 [PUBLIC_SURFACE_MARKER]
526
+ */
527
+ function publicSurfaceFromDecorators(decorators) {
528
+ let publicSurface = false;
529
+ for (const d of decorators) {
530
+ const text = (d?.getText?.() ?? "").trim();
531
+ const name = /^@([A-Za-z_$][\w$]*)/.exec(text)?.[1];
532
+ if (!name)
533
+ continue;
534
+ if (PUBLIC_SURFACE_DECORATOR.test(name)) {
535
+ publicSurface = true;
536
+ break;
537
+ }
538
+ }
539
+ return publicSurface ? [PUBLIC_SURFACE_MARKER] : [];
540
+ }
541
+ // ═══════════════════════════════════════════════════════════════
503
542
  // E3(2026-09-22):TS 侧「入参已按 schema 校验」语义标记
504
543
  //
505
544
  // 判据(先抄准再写 expect,R22):
@@ -3303,6 +3342,15 @@ function _extractSingleProject(absRoot, tsconfigPath) {
3303
3342
  if (!mCalls.includes(mk))
3304
3343
  mCalls.push(mk);
3305
3344
  }
3345
+ // 修冤枉第十刀:@Public()/@SkipAuth()/@AllowAnonymous → 公开面标记
3346
+ // (与 E2/第六刀并列;类级公开装饰器作用于全部方法)
3347
+ for (const mk of publicSurfaceFromDecorators([
3348
+ ...(cls.getDecorators?.() ?? []),
3349
+ ...(m.getDecorators?.() ?? []),
3350
+ ])) {
3351
+ if (!mCalls.includes(mk))
3352
+ mCalls.push(mk);
3353
+ }
3306
3354
  funcs.push({
3307
3355
  name: `${cn}.${mn}`,
3308
3356
  params: mParams.map((p) => ({ name: p.getName(), type: getParamType(p), typeDetail: getParamTypeDetail(p) })),
@@ -0,0 +1,154 @@
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-16)单元测试。
7
+ *
8
+ * 判据(见 computeMixedSurfacePropagatedSet 注释):
9
+ * 函数 F 的每个直接调用方都 ∈ {已守卫传播集} ∪ {作业面传播集} ⇒
10
+ * F 无未鉴权请求面(每条到达 F 的路径要么已过守卫、要么源起作业)。
11
+ * R1-R5:调用方非空 / 边全解析 / 全部落在并集内 / 快照轮次默认 1 /
12
+ * 返回集合只含并集新增成员。
13
+ * 消费方(engine)负责 R5/R6:只压 authorization 类、且不压直接标记函数。
14
+ */
15
+ const MG = "__progmune_auth_machinery__";
16
+ const MJ = "__progmune_no_request_surface__";
17
+ /** 便捷:guard/job 集 + 混合集一并算 */
18
+ function sets(fns, opts) {
19
+ const guard = (0, protocol_detector_1.computeGuardPropagatedSet)(fns);
20
+ const job = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
21
+ const mixed = (0, protocol_detector_1.computeMixedSurfacePropagatedSet)(fns, guard, job, opts);
22
+ return { guard, job, mixed };
23
+ }
24
+ (0, vitest_1.describe)("computeMixedSurfacePropagatedSet(调用方混合并集,第九刀)", () => {
25
+ (0, vitest_1.it)("混合调用方(已守卫 + 作业)⇒ 豁免", () => {
26
+ const fns = [
27
+ { name: "GuardedController.create", file: "c/page.controller.ts", calls: [MG, "log"] },
28
+ { name: "AuditService.log", file: "s/audit.service.ts", calls: ["fn"] },
29
+ { name: "DigestJob.run", file: "q/digest.job.ts", calls: [MJ, "fn"] },
30
+ { name: "MixedTarget.fn", file: "m/target.ts", calls: [] },
31
+ ];
32
+ // AuditService.log:全部调用方已守卫 ⇒ 守卫集成员;
33
+ // MixedTarget.fn 调用方 = {AuditService.log[g], DigestJob.run[j]} ⇒ 混合豁免
34
+ const { guard, job, mixed } = sets(fns);
35
+ (0, vitest_1.expect)(guard.has("AuditService.log")).toBe(true);
36
+ (0, vitest_1.expect)(job.has("DigestJob.run")).toBe(true);
37
+ (0, vitest_1.expect)(mixed.has("MixedTarget.fn")).toBe(true);
38
+ });
39
+ (0, vitest_1.it)("全部调用方已守卫但超过守卫传播深度(第 4 跳)⇒ 并集豁免", () => {
40
+ const fns = [
41
+ { name: "AuthRoot", file: "f.ts", calls: [MG, "b"] },
42
+ { name: "b", file: "f.ts", calls: ["c"] },
43
+ { name: "c", file: "f.ts", calls: ["d"] },
44
+ { name: "d", file: "f.ts", calls: ["e"] },
45
+ { name: "e", file: "f.ts", calls: [] },
46
+ ];
47
+ const { guard, mixed } = sets(fns);
48
+ (0, vitest_1.expect)(guard.has("d")).toBe(true); // 第 3 跳:在守卫集内
49
+ (0, vitest_1.expect)(guard.has("e")).toBe(false); // 第 4 跳:守卫传播边界外
50
+ (0, vitest_1.expect)(mixed.has("e")).toBe(true); // 并集判据罩住(调用方 d ∈ 守卫集)
51
+ });
52
+ (0, vitest_1.it)("作业调用方与作业面集族级保守的差异:按边解析只豁免作业链成员", () => {
53
+ // docmost 实测形态:作业处理器与被调方同「类根」(processExpiring
54
+ // → getPageContext),逐边消歧可解析。B 判据(第七刀)在裸名族含
55
+ // HTTP 调用方时全族不豁免;并集判据按边解析——只豁免作业链上
56
+ // 那个成员,HTTP 面成员照报。
57
+ const fns = [
58
+ { name: "VerificationNotificationService.processExpiring", file: "s/verification.service.ts", calls: [MJ, "getPageContext"] },
59
+ { name: "VerificationNotificationService.getPageContext", file: "s/verification.service.ts", calls: [] },
60
+ { name: "PageNotificationService.getPageContext", file: "s/page.service.ts", calls: [] },
61
+ { name: "PageNotificationService.notify", file: "s/page.service.ts", calls: ["getPageContext"] }, // 同文件 HTTP 面调用方
62
+ ];
63
+ const { job, mixed } = sets(fns);
64
+ // 第七刀 B 判据:裸名族含非作业调用方 ⇒ 全族不豁免
65
+ (0, vitest_1.expect)(job.has("VerificationNotificationService.getPageContext")).toBe(false);
66
+ (0, vitest_1.expect)(job.has("PageNotificationService.getPageContext")).toBe(false);
67
+ // 第九刀并集:边解析逐条——Verification 成员只被作业调用 ⇒ 豁免;
68
+ // Page 成员被 notify(并集外)调用 ⇒ 不豁免
69
+ (0, vitest_1.expect)(mixed.has("VerificationNotificationService.getPageContext")).toBe(true);
70
+ (0, vitest_1.expect)(mixed.has("PageNotificationService.getPageContext")).toBe(false);
71
+ });
72
+ (0, vitest_1.it)("任一调用方在并集外 ⇒ 不豁免(fail-safe)", () => {
73
+ const fns = [
74
+ { name: "GuardedController.create", file: "c.ts", calls: [MG, "write"] },
75
+ { name: "PublicController.import", file: "c2.ts", calls: ["write"] },
76
+ { name: "write", file: "r.ts", calls: [] },
77
+ ];
78
+ const { mixed } = sets(fns);
79
+ (0, vitest_1.expect)(mixed.has("write")).toBe(false);
80
+ });
81
+ (0, vitest_1.it)("无调用方(入口/未连通)⇒ 不豁免(R1)", () => {
82
+ const fns = [{ name: "orphanEntry", file: "m.ts", calls: [] }];
83
+ const { mixed } = sets(fns);
84
+ (0, vitest_1.expect)(mixed.has("orphanEntry")).toBe(false);
85
+ });
86
+ (0, vitest_1.it)("歧义调用边 ⇒ 不豁免(R2 fail-safe)", () => {
87
+ // 同名裸名两个候选、调用方无法消歧(不同文件不同目录不同类根)
88
+ const fns = [
89
+ { name: "GuardedController.create", file: "c/ctrl.ts", calls: [MG, "load"] },
90
+ { name: "XxxService.load", file: "x/x.service.ts", calls: [] },
91
+ { name: "YyyService.load", file: "y/y.service.ts", calls: [] },
92
+ ];
93
+ const { mixed } = sets(fns);
94
+ (0, vitest_1.expect)(mixed.has("XxxService.load")).toBe(false);
95
+ (0, vitest_1.expect)(mixed.has("YyyService.load")).toBe(false);
96
+ });
97
+ (0, vitest_1.it)("默认 1 轮:并集下游的下游不豁免(传递闭包未开)", () => {
98
+ const fns = [
99
+ { name: "Job.run", file: "q.ts", calls: [MJ, "b"] },
100
+ { name: "b", file: "f.ts", calls: ["c"] },
101
+ { name: "c", file: "f.ts", calls: ["d"] },
102
+ { name: "d", file: "f.ts", calls: [] },
103
+ ];
104
+ const { job, mixed } = sets(fns);
105
+ (0, vitest_1.expect)(job.has("b")).toBe(true); // b 已在作业面集(第七刀,第 1 跳)
106
+ (0, vitest_1.expect)(mixed.has("b")).toBe(false); // R5:既有成员不重复进 mixed
107
+ (0, vitest_1.expect)(mixed.has("c")).toBe(true); // c 调用方 {b[j]} ⊆ 并集 ⇒ 混合豁免
108
+ (0, vitest_1.expect)(mixed.has("d")).toBe(false); // d 调用方 {c} 不在并集(1 轮)⇒ 不豁免
109
+ });
110
+ (0, vitest_1.it)("maxRounds=2 ⇒ 传递两跳成立(快照轮次语义)", () => {
111
+ const fns = [
112
+ { name: "Job.run", file: "q.ts", calls: [MJ, "b"] },
113
+ { name: "b", file: "f.ts", calls: ["c"] },
114
+ { name: "c", file: "f.ts", calls: ["d"] },
115
+ { name: "d", file: "f.ts", calls: [] },
116
+ ];
117
+ const { mixed } = sets(fns, { maxRounds: 2 });
118
+ (0, vitest_1.expect)(mixed.has("c")).toBe(true);
119
+ (0, vitest_1.expect)(mixed.has("d")).toBe(true); // 第 2 轮:d 调用方 {c} ⊆ 并集∪第 1 轮新增
120
+ });
121
+ (0, vitest_1.it)("返回集合只含并集新增成员(R5:种子/既有成员由各自刀处理)", () => {
122
+ const fns = [
123
+ { name: "GuardedController.create", file: "c.ts", calls: [MG, "service"] },
124
+ { name: "Job.run", file: "q.ts", calls: [MJ, "jobOnly"] },
125
+ { name: "service", file: "s.ts", calls: ["target"] },
126
+ { name: "jobOnly", file: "s.ts", calls: ["target"] },
127
+ { name: "target", file: "s.ts", calls: [] },
128
+ ];
129
+ const { guard, job, mixed } = sets(fns);
130
+ // 种子/既有成员(GuardedController.create、Job.run、service、jobOnly)不重复进 mixed
131
+ (0, vitest_1.expect)(guard.has("service")).toBe(true); // service 全部调用方已守卫 ⇒ 守卫集成员
132
+ (0, vitest_1.expect)(job.has("jobOnly")).toBe(true); // jobOnly 只被作业调用 ⇒ 作业面成员
133
+ (0, vitest_1.expect)(mixed.has("GuardedController.create")).toBe(false);
134
+ (0, vitest_1.expect)(mixed.has("Job.run")).toBe(false);
135
+ (0, vitest_1.expect)(mixed.has("service")).toBe(false);
136
+ (0, vitest_1.expect)(mixed.has("jobOnly")).toBe(false);
137
+ // 新增成员:target 调用方 = {service[g], jobOnly[j]} ⇒ 混合豁免
138
+ (0, vitest_1.expect)(mixed.has("target")).toBe(true);
139
+ });
140
+ (0, vitest_1.it)("自调不提供豁免证据", () => {
141
+ const fns = [{ name: "CleanupService.cleanup", file: "s.ts", calls: ["cleanup"] }];
142
+ const { mixed } = sets(fns);
143
+ (0, vitest_1.expect)(mixed.has("CleanupService.cleanup")).toBe(false);
144
+ });
145
+ (0, vitest_1.it)("直接标记函数不进入混合新增集合(自身已是守卫/作业种子)", () => {
146
+ const fns = [
147
+ { name: "GuardedController.create", file: "c.ts", calls: [MG, "self2"] },
148
+ { name: "self2", file: "c.ts", calls: [] },
149
+ ];
150
+ const { mixed } = sets(fns);
151
+ (0, vitest_1.expect)(mixed.has("GuardedController.create")).toBe(false);
152
+ (0, vitest_1.expect)(mixed.has("self2")).toBe(false); // self2 已在守卫集(第 1 跳)
153
+ });
154
+ });
@@ -0,0 +1,48 @@
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-16):公开面标记的规则侧接受位锁定。
7
+ *
8
+ * 语义:@Public()/@SkipAuth()/@AllowAnonymous 是开发者显式声明免鉴权
9
+ * (正向证据,R75)——授权族四条的「缺鉴权/缺属主校验」提问在公开
10
+ * 端点上不适用。只抑制 authorization 类;其他族规则不受标记影响。
11
+ */
12
+ const M = "__progmune_public_surface__";
13
+ const has = (vs, rule) => vs.some((v) => v.rule === rule);
14
+ (0, vitest_1.describe)("public_surface 标记:授权族四条接受、其他族不受影响", () => {
15
+ const calls = ["getShareTree", M];
16
+ (0, vitest_1.it)("Unauthenticated Access 被抑制", () => {
17
+ const bare = (0, protocol_detector_1.detectSafeguardViolations)(["getShareTree"], "ShareController.getShare", "typescript", ["shareId"], true);
18
+ (0, vitest_1.expect)(has(bare, "Authorization (Unauthenticated Access)")).toBe(true);
19
+ const withMark = (0, protocol_detector_1.detectSafeguardViolations)(calls, "ShareController.getShare", "typescript", ["shareId"], true);
20
+ (0, vitest_1.expect)(has(withMark, "Authorization (Unauthenticated Access)")).toBe(false);
21
+ });
22
+ (0, vitest_1.it)("Unauthenticated Mutation 被抑制", () => {
23
+ // updateInvite 直接命中 Mutation 触发词;真实语料里的 acceptInvite
24
+ // 告警靠函数体内调用上下文触发,隔离夹具用直接形态
25
+ const bare = (0, protocol_detector_1.detectSafeguardViolations)(["updateInvite"], "WorkspaceController.updateInvite", "typescript", ["token"], true);
26
+ (0, vitest_1.expect)(has(bare, "Authorization (Unauthenticated Mutation)")).toBe(true);
27
+ const withMark = (0, protocol_detector_1.detectSafeguardViolations)(["updateInvite", M], "WorkspaceController.updateInvite", "typescript", ["token"], true);
28
+ (0, vitest_1.expect)(has(withMark, "Authorization (Unauthenticated Mutation)")).toBe(false);
29
+ });
30
+ (0, vitest_1.it)("Ownership Check 被抑制(公开端点的属主校验提问不适用)", () => {
31
+ // sharePage 直接命中 Ownership 触发词(share+Page);真实语料里的
32
+ // getSharedPageInfo 类告警靠同文件级调用上下文触发,隔离夹具用直接形态
33
+ const bare = (0, protocol_detector_1.detectSafeguardViolations)(["sharePage"], "ShareController.sharePage", "typescript", ["shareId"], true);
34
+ (0, vitest_1.expect)(has(bare, "Authorization (Ownership Check)")).toBe(true);
35
+ const withMark = (0, protocol_detector_1.detectSafeguardViolations)(["sharePage", M], "ShareController.sharePage", "typescript", ["shareId"], true);
36
+ (0, vitest_1.expect)(has(withMark, "Authorization (Ownership Check)")).toBe(false);
37
+ });
38
+ (0, vitest_1.it)("Resource Ownership 被抑制", () => {
39
+ const bare = (0, protocol_detector_1.detectSafeguardViolations)(["toggleShare"], "ShareController.toggleShare", "typescript", ["id"], true);
40
+ (0, vitest_1.expect)(has(bare, "Authorization (Resource Ownership)")).toBe(true);
41
+ const withMark = (0, protocol_detector_1.detectSafeguardViolations)(["toggleShare", M], "ShareController.toggleShare", "typescript", ["id"], true);
42
+ (0, vitest_1.expect)(has(withMark, "Authorization (Resource Ownership)")).toBe(false);
43
+ });
44
+ (0, vitest_1.it)("非授权族规则不受标记影响(渲染 sink 照报)", () => {
45
+ const vs = (0, protocol_detector_1.detectSafeguardViolations)(["render", M], "PublicController.renderTemplate", "typescript");
46
+ (0, vitest_1.expect)(has(vs, "No Input Sanitization")).toBe(true);
47
+ });
48
+ });
@@ -0,0 +1,120 @@
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-16)单元测试。
7
+ *
8
+ * 判据(见 computePublicSurfacePropagatedSet 注释):
9
+ * 函数 F 的每个直接调用方都 ∈ {公开面} ∪ {已守卫传播集} ∪
10
+ * {作业面传播集} ⇒ F 无未鉴权请求面(公开=开发者显式声明,
11
+ * 已守卫=过了鉴权,作业=无请求面)。
12
+ * R1-R5:调用方非空 / 边全解析 / 全部落在并集内 / 快照轮次默认 1 /
13
+ * 返回集合只含并集新增成员。
14
+ * 直接公开标记函数自身的告警由规则侧 safeguard 接受标记消除(R1 注)。
15
+ */
16
+ const MG = "__progmune_auth_machinery__";
17
+ const MJ = "__progmune_no_request_surface__";
18
+ /** 便捷:guard/job/公开集 + 公开传播集一并算 */
19
+ function sets(fns, publicFns, opts) {
20
+ const guard = (0, protocol_detector_1.computeGuardPropagatedSet)(fns);
21
+ const job = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
22
+ const pub = new Set(publicFns);
23
+ const propagated = (0, protocol_detector_1.computePublicSurfacePropagatedSet)(fns, pub, guard, job, opts);
24
+ return { guard, job, pub, propagated };
25
+ }
26
+ (0, vitest_1.describe)("computePublicSurfacePropagatedSet(公开表面传播,第十刀)", () => {
27
+ (0, vitest_1.it)("只被公开端点调用 ⇒ 豁免", () => {
28
+ const fns = [
29
+ { name: "ShareController.getShare", file: "c/share.controller.ts", calls: ["getShareTree"] },
30
+ { name: "ShareService.getShareTree", file: "s/share.service.ts", calls: [] },
31
+ ];
32
+ const { propagated } = sets(fns, ["ShareController.getShare"]);
33
+ (0, vitest_1.expect)(propagated.has("ShareService.getShareTree")).toBe(true);
34
+ });
35
+ (0, vitest_1.it)("混合调用方(公开 + 已守卫)⇒ 豁免(第九刀够不着的形态)", () => {
36
+ const fns = [
37
+ { name: "GuardedController.create", file: "c/page.controller.ts", calls: [MG, "searchPage"] },
38
+ { name: "SearchController.searchShare", file: "c/search.controller.ts", calls: ["searchPage"] },
39
+ { name: "SearchService.searchPage", file: "s/search.service.ts", calls: [] },
40
+ ];
41
+ const { guard, propagated } = sets(fns, ["SearchController.searchShare"]);
42
+ (0, vitest_1.expect)(guard.has("SearchService.searchPage")).toBe(false); // 非全守卫 ⇒ 第五刀不豁免
43
+ (0, vitest_1.expect)(propagated.has("SearchService.searchPage")).toBe(true); // 公开∪守卫 并集 ⇒ 豁免
44
+ });
45
+ (0, vitest_1.it)("混合调用方(公开 + 作业)⇒ 豁免", () => {
46
+ const fns = [
47
+ { name: "ShareController.getShare", file: "c.ts", calls: ["getShareTree"] },
48
+ { name: "DigestJob.run", file: "q.ts", calls: [MJ, "getShareTree"] },
49
+ { name: "ShareService.getShareTree", file: "s.ts", calls: [] },
50
+ ];
51
+ const { propagated } = sets(fns, ["ShareController.getShare"]);
52
+ (0, vitest_1.expect)(propagated.has("ShareService.getShareTree")).toBe(true);
53
+ });
54
+ (0, vitest_1.it)("任一调用方在并集外(未守卫的非公开调用方)⇒ 不豁免(fail-safe)", () => {
55
+ const fns = [
56
+ { name: "ShareController.getShare", file: "c.ts", calls: ["getShareTree"] },
57
+ { name: "InternalController.export", file: "c2.ts", calls: ["getShareTree"] }, // 无守卫无公开标记
58
+ { name: "ShareService.getShareTree", file: "s.ts", calls: [] },
59
+ ];
60
+ const { propagated } = sets(fns, ["ShareController.getShare"]);
61
+ (0, vitest_1.expect)(propagated.has("ShareService.getShareTree")).toBe(false);
62
+ });
63
+ (0, vitest_1.it)("无调用方(入口/未连通)⇒ 不豁免(R1)", () => {
64
+ const fns = [{ name: "orphanEntry", file: "m.ts", calls: [] }];
65
+ const { propagated } = sets(fns, ["orphanEntry"]);
66
+ (0, vitest_1.expect)(propagated.has("orphanEntry")).toBe(false);
67
+ });
68
+ (0, vitest_1.it)("歧义调用边 ⇒ 不豁免(R2 fail-safe)", () => {
69
+ const fns = [
70
+ { name: "PublicController.get", file: "c/ctrl.ts", calls: ["load"] },
71
+ { name: "XxxService.load", file: "x/x.service.ts", calls: [] },
72
+ { name: "YyyService.load", file: "y/y.service.ts", calls: [] },
73
+ ];
74
+ const { propagated } = sets(fns, ["PublicController.get"]);
75
+ (0, vitest_1.expect)(propagated.has("XxxService.load")).toBe(false);
76
+ (0, vitest_1.expect)(propagated.has("YyyService.load")).toBe(false);
77
+ });
78
+ (0, vitest_1.it)("默认 1 轮:公开下游的下游不豁免(传递闭包未开)", () => {
79
+ const fns = [
80
+ { name: "ShareController.getShare", file: "c.ts", calls: ["getShareTree"] },
81
+ { name: "ShareService.getShareTree", file: "s.ts", calls: ["loadPageData"] },
82
+ { name: "loadPageData", file: "s.ts", calls: [] },
83
+ ];
84
+ const { propagated } = sets(fns, ["ShareController.getShare"]);
85
+ (0, vitest_1.expect)(propagated.has("ShareService.getShareTree")).toBe(true);
86
+ (0, vitest_1.expect)(propagated.has("loadPageData")).toBe(false);
87
+ });
88
+ (0, vitest_1.it)("maxRounds=2 ⇒ 传递两跳成立(快照轮次语义)", () => {
89
+ const fns = [
90
+ { name: "ShareController.getShare", file: "c.ts", calls: ["getShareTree"] },
91
+ { name: "ShareService.getShareTree", file: "s.ts", calls: ["loadPageData"] },
92
+ { name: "loadPageData", file: "s.ts", calls: [] },
93
+ ];
94
+ const { propagated } = sets(fns, ["ShareController.getShare"], { maxRounds: 2 });
95
+ (0, vitest_1.expect)(propagated.has("ShareService.getShareTree")).toBe(true);
96
+ (0, vitest_1.expect)(propagated.has("loadPageData")).toBe(true);
97
+ });
98
+ (0, vitest_1.it)("返回集合只含并集新增成员(R5:公开种子/守卫/作业成员不重复进)", () => {
99
+ const fns = [
100
+ { name: "PublicController.show", file: "c.ts", calls: ["target"] },
101
+ { name: "GuardedController.create", file: "c.ts", calls: [MG, "service"] },
102
+ { name: "Job.run", file: "q.ts", calls: [MJ, "jobOnly"] },
103
+ { name: "service", file: "s.ts", calls: ["target"] },
104
+ { name: "jobOnly", file: "s.ts", calls: ["target"] },
105
+ { name: "target", file: "s.ts", calls: [] },
106
+ ];
107
+ const { propagated } = sets(fns, ["PublicController.show"]);
108
+ // 公开种子自身、守卫/作业成员都不重复进 propagated
109
+ (0, vitest_1.expect)(propagated.has("PublicController.show")).toBe(false);
110
+ (0, vitest_1.expect)(propagated.has("Job.run")).toBe(false);
111
+ (0, vitest_1.expect)(propagated.has("service")).toBe(false); // 守卫传播成员(调用方全守卫)
112
+ (0, vitest_1.expect)(propagated.has("jobOnly")).toBe(false); // 作业面成员
113
+ (0, vitest_1.expect)(propagated.has("target")).toBe(true); // 调用方 {public[pub], service[g], jobOnly[j]} ⊆ 并集
114
+ });
115
+ (0, vitest_1.it)("自调不提供豁免证据", () => {
116
+ const fns = [{ name: "ShareService.getShareTree", file: "s.ts", calls: ["getShareTree"] }];
117
+ const { propagated } = sets(fns, []);
118
+ (0, vitest_1.expect)(propagated.has("ShareService.getShareTree")).toBe(false);
119
+ });
120
+ });
@@ -14,8 +14,11 @@ exports.isAuthFunctionName = isAuthFunctionName;
14
14
  exports.classifyParamType = classifyParamType;
15
15
  exports.detectSafeguardViolations = detectSafeguardViolations;
16
16
  exports.buildCallerMap = buildCallerMap;
17
+ exports.computeResolvedCallerMap = computeResolvedCallerMap;
17
18
  exports.computeGuardPropagatedSet = computeGuardPropagatedSet;
18
19
  exports.computeJobSurfacePropagatedSet = computeJobSurfacePropagatedSet;
20
+ exports.computeMixedSurfacePropagatedSet = computeMixedSurfacePropagatedSet;
21
+ exports.computePublicSurfacePropagatedSet = computePublicSurfacePropagatedSet;
19
22
  exports.detectSafeguardViolationsV7 = detectSafeguardViolationsV7;
20
23
  exports.detectProtocolViolations = detectProtocolViolations;
21
24
  exports.validateProtocolState = validateProtocolState;
@@ -388,7 +391,7 @@ const SAFEGUARD_RULES = [
388
391
  { pattern: /\b(hasPermission|checkPermission|checkAccess|isAuthorized|checkRole|requireRole|adminCheck|isAdmin|canModify|canDelete|canEdit)\b/i, label: "authz_check" },
389
392
  // 修冤枉第六刀:队列/定时/事件处理器不面向请求面——鉴权发生在入队侧,
390
393
  // 「改资源没查属主」的提问在处理器层不适用(docmost 补标注 FP 类)
391
- { pattern: /\b(__progmune_no_request_surface__)\b/, label: "no_request_surface" },
394
+ { pattern: /\b(__progmune_no_request_surface__|__progmune_public_surface__)\b/, label: "no_request_surface" },
392
395
  { pattern: /\b(__progmune_ownership_checked__)\b/, label: "inline_ownership_check" },
393
396
  // §46 ②:资源属主被**赋值为当前主体**(`{ userId: user.id }` / `where:{userId: user.id}`)。
394
397
  // 这类函数的被操作对象就是(或已被限定在)主体自己,谈不上「改别人的东西」。
@@ -432,7 +435,7 @@ const SAFEGUARD_RULES = [
432
435
  paramTypeSafeguards: [SESSION_BEARING_TYPE_RE],
433
436
  trigger: /\b(list|download|view|fetch)(?:[A-Z]\w*|_\w+)|get(?:[A-Z]\w+|_\w+)/i,
434
437
  safeguards: [
435
- { pattern: /\b(getUser|validateToken|verifyToken|verifySession|validateSession|getSessionUser|getSession\b|getCurrentUser|token\w*(Check|Verify|Valid)|session\w*(Check|Verify|Valid)|auth\w*(Check|Verify|Valid|Guard|Middleware|Required)|requireAuth|withAuth|authenticate\w*(User|Request|Token)?|checkAuth|isAuth|hasAuth|checkAccess|hasAccess|get_user|get_session_user|get_current_user|validate_session|verify_token|require_auth|with_auth|check_auth|auth_required|authenticate_user|authenticate_request|authenticate_token|token_check|token_verify|token_valid|session_check|session_verify|session_valid|auth_check|auth_guard|auth_middleware|get_current_user_authorizer|current_user_authorizer|login_required|permission_required|user_passes_test|check_authorization|check_permission|jwt\.decode|decode_token|__progmune_auth_checked__|__progmune_credential_check__|__progmune_drf_permissions__|__progmune_auth_machinery__|__progmune_no_request_surface__)\b/i, label: "auth_check" },
438
+ { pattern: /\b(getUser|validateToken|verifyToken|verifySession|validateSession|getSessionUser|getSession\b|getCurrentUser|token\w*(Check|Verify|Valid)|session\w*(Check|Verify|Valid)|auth\w*(Check|Verify|Valid|Guard|Middleware|Required)|requireAuth|withAuth|authenticate\w*(User|Request|Token)?|checkAuth|isAuth|hasAuth|checkAccess|hasAccess|get_user|get_session_user|get_current_user|validate_session|verify_token|require_auth|with_auth|check_auth|auth_required|authenticate_user|authenticate_request|authenticate_token|token_check|token_verify|token_valid|session_check|session_verify|session_valid|auth_check|auth_guard|auth_middleware|get_current_user_authorizer|current_user_authorizer|login_required|permission_required|user_passes_test|check_authorization|check_permission|jwt\.decode|decode_token|__progmune_auth_checked__|__progmune_credential_check__|__progmune_drf_permissions__|__progmune_auth_machinery__|__progmune_no_request_surface__|__progmune_public_surface__)\b/i, label: "auth_check" },
436
439
  // §40 C1:裸 can —— verdaccio 的 can('publish') 中间件就是这里的授权判定
437
440
  { pattern: exports.AUTHZ_BARE_CAN_RE, label: "bare_can", callsOnly: true },
438
441
  ],
@@ -471,7 +474,7 @@ const SAFEGUARD_RULES = [
471
474
  // (listPosts/getPost/deletePost fire via identifier-parsed words).
472
475
  trigger: /\b(add|create|update|set|publish|insert|submit)(?:[A-Z]\w*|_\w+)|(?:[A-Z]\w*|_\w+)(Add|Create|Update|Set|Publish|Insert|Submit)\b/i,
473
476
  safeguards: [
474
- { pattern: /\b(getUser|validateToken|verifyToken|verifySession|validateSession|getSessionUser|getSession\b|getCurrentUser|token\w*(Check|Verify|Valid)|session\w*(Check|Verify|Valid)|auth\w*(Check|Verify|Valid|Guard|Middleware|Required)|requireAuth|withAuth|authenticate\w*(User|Request|Token)?|checkAuth|isAuth|hasAuth|checkAccess|hasAccess|get_user|get_session_user|get_current_user|validate_session|verify_token|require_auth|with_auth|check_auth|auth_required|authenticate_user|authenticate_request|authenticate_token|token_check|token_verify|token_valid|session_check|session_verify|session_valid|auth_check|auth_guard|auth_middleware|get_current_user_authorizer|current_user_authorizer|login_required|permission_required|user_passes_test|check_authorization|check_permission|__progmune_auth_checked__|__progmune_credential_check__|__progmune_drf_permissions__|__progmune_auth_machinery__|__progmune_no_request_surface__)\b/i, label: "auth_check" },
477
+ { pattern: /\b(getUser|validateToken|verifyToken|verifySession|validateSession|getSessionUser|getSession\b|getCurrentUser|token\w*(Check|Verify|Valid)|session\w*(Check|Verify|Valid)|auth\w*(Check|Verify|Valid|Guard|Middleware|Required)|requireAuth|withAuth|authenticate\w*(User|Request|Token)?|checkAuth|isAuth|hasAuth|checkAccess|hasAccess|get_user|get_session_user|get_current_user|validate_session|verify_token|require_auth|with_auth|check_auth|auth_required|authenticate_user|authenticate_request|authenticate_token|token_check|token_verify|token_valid|session_check|session_verify|session_valid|auth_check|auth_guard|auth_middleware|get_current_user_authorizer|current_user_authorizer|login_required|permission_required|user_passes_test|check_authorization|check_permission|__progmune_auth_checked__|__progmune_credential_check__|__progmune_drf_permissions__|__progmune_auth_machinery__|__progmune_no_request_surface__|__progmune_public_surface__)\b/i, label: "auth_check" },
475
478
  // §40 C1:裸 can —— 同上
476
479
  { pattern: exports.AUTHZ_BARE_CAN_RE, label: "bare_can", callsOnly: true },
477
480
  // §46 ①:凭签发入口豁免 —— 独立 safeguard,**不并入 auth_check**。
@@ -641,7 +644,7 @@ const SAFEGUARD_RULES = [
641
644
  { pattern: /\b(ownerId\s*[!=]==?|authorId\s*[!=]==?|userId\s*[!=]==?|createdBy|\.owner\s*[!=]==?)/i, label: "ownership_comparison" },
642
645
  { pattern: /\b(__progmune_ownership_checked__)\b/, label: "inline_ownership_check" },
643
646
  // 修冤枉第六刀:同上(无请求面 ⇒ 属主校验提问不适用)
644
- { pattern: /\b(__progmune_no_request_surface__)\b/, label: "no_request_surface" },
647
+ { pattern: /\b(__progmune_no_request_surface__|__progmune_public_surface__)\b/, label: "no_request_surface" },
645
648
  // §38:同 Ownership Check,组合式谓语一并接上(这条规则更严,更需要)
646
649
  { pattern: exports.AUTHZ_PREDICATE_RE, label: "casl_predicate" },
647
650
  { pattern: exports.AUTHZ_CAN_HELPER_RE, label: "can_helper" },
@@ -1643,31 +1646,17 @@ function buildCallerMap(funcs) {
1643
1646
  return callerMap;
1644
1647
  }
1645
1648
  /**
1646
- * 修冤枉第五刀(2026-10-10):守卫沿调用链传播——
1647
- * 授权族告警的「守卫在 controller 层、名字级看不见」根因的保守豁免。
1649
+ * 反向调用图(分层消歧,R4):被调【全限定名】→ 调用方列表
1650
+ * (resolved=false = 歧义 fail-safe)。computeGuardPropagatedSet(第五刀)
1651
+ * 与 computeMixedSurfacePropagatedSet(第九刀)共享同一边解析。
1648
1652
  *
1649
- * 语义:「函数的**全部**直接调用方都已守卫 ⇒ 该函数视为守卫后」。
1650
- * 证据:docmost 补标注 111 条授权族告警全 FP,其中 49 条(44%)是
1651
- * controller 已 @UseGuards、service/repo 层函数被误报。保守量化
1652
- * (blind-benchmark/auth-guard-propagation-probe.ts):
1653
- * docmost 745 条授权族告警可压 47 条(6.3%),TS 盲测可压 0 条。
1654
- *
1655
- * 保守规则(R1-R4,fail-safe 方向):
1656
- * R1 只有【全部】调用方已守卫才豁免;任一调用方无守卫证据 → 照报
1657
- * R2 守卫沿调用链向下传播 ≤ maxDepth(默认 3)跳
1658
- * R3 传播通道只认 `marker`(默认 __progmune_auth_machinery__)一个标记
1659
- * R4 裸名调用解析分层消歧(唯一才解析,否则给全部候选挂未解析边):
1660
- * ① 名字后缀唯一 ② 同文件唯一 ③ 同目录唯一
1661
- * ④ 类根对齐唯一(调用方 XController/Service → 候选类根同为 X)
1662
- * R6 豁免只给【非直接标记】函数——自身带标记的维持既有 E2 语义
1663
- * (只接受标记的规则由 detector 自身处理;不接受标记的规则照报)
1664
- *
1665
- * 返回集合含直接标记函数本身(消费方须自行应用 R6 过滤),
1666
- * 因为传递闭包的种子就是它们。
1653
+ * 边解析分层消歧(R4):① 后缀唯一 ② 同文件唯一 ③ 同目录唯一
1654
+ * ④ 类根对齐唯一(XController→XService→XRepo 的 MVC 命名惯例);
1655
+ * 全部失败 ⇒ 给全部同名候选挂未解析边(fail-safe)。
1656
+ * 调用方自身排除:controller.create 调 "create" 时自己就是后缀候选
1657
+ * 之一,同文件/同目录层会错误自我捕获(递归/自调不提供守卫证据)。
1667
1658
  */
1668
- function computeGuardPropagatedSet(funcs, opts) {
1669
- const marker = opts?.marker ?? "__progmune_auth_machinery__";
1670
- const maxDepth = opts?.maxDepth ?? 3;
1659
+ function computeResolvedCallerMap(funcs) {
1671
1660
  const byName = new Map();
1672
1661
  for (const f of funcs)
1673
1662
  if (!byName.has(f.name))
@@ -1680,10 +1669,6 @@ function computeGuardPropagatedSet(funcs, opts) {
1680
1669
  bySuffix.set(bare, []);
1681
1670
  bySuffix.get(bare).push(f.name);
1682
1671
  }
1683
- // 反向调用图:被调【全限定名】→ 调用方列表(resolved=false = 歧义 fail-safe)。
1684
- // 边解析分层消歧(R4):① 后缀唯一 ② 同文件唯一 ③ 同目录唯一
1685
- // ④ 类根对齐唯一(XController→XService→XRepo 的 MVC 命名惯例);
1686
- // 全部失败 ⇒ 给全部同名候选挂未解析边(fail-safe)。
1687
1672
  const dirOf = (p) => (p ? p.split("/").slice(0, -1).join("/") : "");
1688
1673
  const rootOf = (n) => {
1689
1674
  const cls = n.split(".")[0] || n;
@@ -1694,8 +1679,6 @@ function computeGuardPropagatedSet(funcs, opts) {
1694
1679
  for (const c of f.calls || []) {
1695
1680
  if (!bySuffix.has(c))
1696
1681
  continue; // 非项目函数名(外部库调用)→ 无守卫语义
1697
- // 排除调用方自身:controller.create 调 "create" 时,自己就是后缀
1698
- // 候选之一,同文件/同目录层会错误自我捕获(递归/自调不提供守卫证据)
1699
1682
  const cands = bySuffix.get(c).filter((n) => n !== f.name);
1700
1683
  if (cands.length === 0)
1701
1684
  continue;
@@ -1740,6 +1723,35 @@ function computeGuardPropagatedSet(funcs, opts) {
1740
1723
  }
1741
1724
  }
1742
1725
  }
1726
+ return calleeToCallers;
1727
+ }
1728
+ /**
1729
+ * 修冤枉第五刀(2026-10-10):守卫沿调用链传播——
1730
+ * 授权族告警的「守卫在 controller 层、名字级看不见」根因的保守豁免。
1731
+ *
1732
+ * 语义:「函数的**全部**直接调用方都已守卫 ⇒ 该函数视为守卫后」。
1733
+ * 证据:docmost 补标注 111 条授权族告警全 FP,其中 49 条(44%)是
1734
+ * controller 已 @UseGuards、service/repo 层函数被误报。保守量化
1735
+ * (blind-benchmark/auth-guard-propagation-probe.ts):
1736
+ * docmost 745 条授权族告警可压 47 条(6.3%),TS 盲测可压 0 条。
1737
+ *
1738
+ * 保守规则(R1-R4,fail-safe 方向):
1739
+ * R1 只有【全部】调用方已守卫才豁免;任一调用方无守卫证据 → 照报
1740
+ * R2 守卫沿调用链向下传播 ≤ maxDepth(默认 3)跳
1741
+ * R3 传播通道只认 `marker`(默认 __progmune_auth_machinery__)一个标记
1742
+ * R4 裸名调用解析分层消歧(唯一才解析,否则给全部候选挂未解析边):
1743
+ * ① 名字后缀唯一 ② 同文件唯一 ③ 同目录唯一
1744
+ * ④ 类根对齐唯一(调用方 XController/Service → 候选类根同为 X)
1745
+ * R6 豁免只给【非直接标记】函数——自身带标记的维持既有 E2 语义
1746
+ * (只接受标记的规则由 detector 自身处理;不接受标记的规则照报)
1747
+ *
1748
+ * 返回集合含直接标记函数本身(消费方须自行应用 R6 过滤),
1749
+ * 因为传递闭包的种子就是它们。
1750
+ */
1751
+ function computeGuardPropagatedSet(funcs, opts) {
1752
+ const marker = opts?.marker ?? "__progmune_auth_machinery__";
1753
+ const maxDepth = opts?.maxDepth ?? 3;
1754
+ const calleeToCallers = computeResolvedCallerMap(funcs);
1743
1755
  // 种子:直接带标记的函数
1744
1756
  const guarded = new Set();
1745
1757
  for (const f of funcs)
@@ -1827,6 +1839,100 @@ function computeJobSurfacePropagatedSet(funcs, opts) {
1827
1839
  }
1828
1840
  return exempt;
1829
1841
  }
1842
+ /**
1843
+ * 修冤枉第九刀(2026-10-16):调用方混合并集豁免——
1844
+ * 「函数的每个直接调用方都 ∈ {已守卫传播集} ∪ {作业面传播集}」。
1845
+ *
1846
+ * 第五刀要求【全部调用方已守卫】,第七刀要求【全部调用方是作业】;
1847
+ * 混合形态(部分调用方是已守卫 controller、部分是无请求面作业)两者都
1848
+ * 覆盖不到,但语义上同样无未鉴权请求面:每条到达 F 的路径要么已过守卫
1849
+ * (guardSet 成员自身及其调用链上方都已守卫),要么源起作业处理器
1850
+ * (jobSet 成员无请求面)。C4 repo 层 update* 语义类与 C5 getter 语义类
1851
+ * 的主力根因就是这种调用方混合(knife8-word-collision-probe 分类结论)。
1852
+ *
1853
+ * 保守规则(与第五刀共享边解析,见 computeResolvedCallerMap R4):
1854
+ * R1 调用方集合非空——入口函数(HTTP 面)无项目调用方,永不豁免
1855
+ * R2 每条调用边都必须可解析(resolved=true),歧义 fail-safe
1856
+ * R3 调用方必须 ∈ guardSet ∪ jobSet;任一调用方不在此并集 → 照报
1857
+ * R4 快照轮次语义(与第五刀一致);默认 1 轮(只豁免直接混合下游;
1858
+ * 传递闭包留数据驱动再开,与第七刀同决策)
1859
+ * R5 返回集合只含并集【新增】成员——guardSet/jobSet 成员由各自刀处理
1860
+ *
1861
+ * guardSet/jobSet 由调用方传入(engine 处与第五/七刀同源:
1862
+ * computeGuardPropagatedSet / computeJobSurfacePropagatedSet 的返回值)。
1863
+ */
1864
+ function computeMixedSurfacePropagatedSet(funcs, guardSet, jobSet, opts) {
1865
+ const maxRounds = opts?.maxRounds ?? 1;
1866
+ const calleeToCallers = computeResolvedCallerMap(funcs);
1867
+ const base = new Set([...guardSet, ...jobSet]);
1868
+ const mixed = new Set(); // R5:只记本轮新增
1869
+ for (let round = 0; round < maxRounds; round++) {
1870
+ const toAdd = [];
1871
+ for (const f of funcs) {
1872
+ if (base.has(f.name))
1873
+ continue;
1874
+ const callers = calleeToCallers.get(f.name) || [];
1875
+ if (callers.length === 0)
1876
+ continue; // R1:无调用方(入口/未连通)→ 不豁免
1877
+ if (callers.every((c) => c.resolved && base.has(c.caller)))
1878
+ toAdd.push(f.name);
1879
+ }
1880
+ if (toAdd.length === 0)
1881
+ break;
1882
+ for (const n of toAdd) {
1883
+ base.add(n);
1884
+ mixed.add(n);
1885
+ }
1886
+ }
1887
+ return mixed;
1888
+ }
1889
+ /**
1890
+ * 修冤枉第十刀(2026-10-16):公开表面传播豁免——
1891
+ * 「函数的每个直接调用方都 ∈ {公开面传播集} ∪ {已守卫传播集} ∪
1892
+ * {作业面传播集}」。
1893
+ *
1894
+ * 第九刀覆盖「守卫∪作业」混合;公开端点(@Public()/@SkipAuth()/
1895
+ * @AllowAnonymous 显式免鉴权装饰器)是第三条正当路径——公开是开发者
1896
+ * **显式声明**(正向证据,R75):经由公开端点可达的函数,授权族提问
1897
+ * 「缺鉴权」不适用(公开≠意外缺守卫;未公开却缺守卫照报)。
1898
+ *
1899
+ * 保守规则(与第五/九刀共享边解析,见 computeResolvedCallerMap R4):
1900
+ * R1 调用方集合非空——入口函数无项目调用方,永不豁免(公开入口
1901
+ * 自身的告警由规则侧 safeguard 接受标记直接消除,与第六刀同架构)
1902
+ * R2 每条调用边都必须可解析(resolved=true),歧义 fail-safe
1903
+ * R3 调用方必须 ∈ publicSet ∪ guardSet ∪ jobSet;任一在并集外 → 照报
1904
+ * R4 快照轮次语义;默认 1 轮
1905
+ * R5 返回集合只含并集【新增】成员
1906
+ *
1907
+ * 注意:并集含守卫∪作业 ⇒ 本集合是第九刀的超集;engine 消费顺序
1908
+ * guard→job→mixed→public(未豁免才挂 public 标记)保证第九刀告警
1909
+ * 不重复挂标记。
1910
+ */
1911
+ function computePublicSurfacePropagatedSet(funcs, publicSet, guardSet, jobSet, opts) {
1912
+ const maxRounds = opts?.maxRounds ?? 1;
1913
+ const calleeToCallers = computeResolvedCallerMap(funcs);
1914
+ const base = new Set([...publicSet, ...guardSet, ...jobSet]);
1915
+ const added = new Set(); // R5:只记本轮新增
1916
+ for (let round = 0; round < maxRounds; round++) {
1917
+ const toAdd = [];
1918
+ for (const f of funcs) {
1919
+ if (base.has(f.name))
1920
+ continue;
1921
+ const callers = calleeToCallers.get(f.name) || [];
1922
+ if (callers.length === 0)
1923
+ continue; // R1:无调用方(入口/未连通)→ 不豁免
1924
+ if (callers.every((c) => c.resolved && base.has(c.caller)))
1925
+ toAdd.push(f.name);
1926
+ }
1927
+ if (toAdd.length === 0)
1928
+ break;
1929
+ for (const n of toAdd) {
1930
+ base.add(n);
1931
+ added.add(n);
1932
+ }
1933
+ }
1934
+ return added;
1935
+ }
1830
1936
  /**
1831
1937
  * v7: Build safeguard context — own → same-file → caller chain.
1832
1938
  *
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.67";
23
+ exports.RUNTIME_VERSION = "3.7.69";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
@@ -1512,6 +1512,29 @@ async function collectProtocolViolations(ctx, callGraph) {
1512
1512
  const jobSurfacePropagated = process.env.PROGMUNE_JOB_SURFACE_TOGGLE === "off"
1513
1513
  ? new Set()
1514
1514
  : (0, protocol_detector_1.computeJobSurfacePropagatedSet)(functions);
1515
+ // 修冤枉第九刀(2026-10-16):调用方混合并集豁免——「每个直接
1516
+ // 调用方都 ∈ {已守卫传播集} ∪ {作业面传播集}」⇒ 无未鉴权请求面。
1517
+ // 保守判据见 computeMixedSurfacePropagatedSet(R1-R5):调用方
1518
+ // 非空、边全解析、默认 1 轮。豁免仅作用于 authorization 类且
1519
+ // 函数自身不直接带标记(R5/R6)——与第五/七刀同:标记而非丢弃。
1520
+ const mixedSurfacePropagated = process.env.PROGMUNE_MIXED_SURFACE_TOGGLE === "off"
1521
+ ? new Set()
1522
+ : (0, protocol_detector_1.computeMixedSurfacePropagatedSet)(functions, guardPropagated, jobSurfacePropagated);
1523
+ // 修冤枉第十刀(2026-10-16):公开表面传播豁免——「每个直接
1524
+ // 调用方都 ∈ {公开面} ∪ {已守卫} ∪ {作业面}」⇒ 无未鉴权请求面
1525
+ // (公开=开发者显式声明,见 computePublicSurfacePropagatedSet
1526
+ // R1-R5)。直接公开标记函数的告警由规则侧 safeguard 接受标记
1527
+ // 消除(与第六刀同架构);本传播只覆盖下游。并集含守卫∪作业
1528
+ // ⇒ 是第九刀超集——消费顺序 guard→job→mixed→public,已豁免
1529
+ // 的告警不重复挂 public 标记。
1530
+ const publicMarked = new Set();
1531
+ for (const f of functions) {
1532
+ if ((f.calls || []).includes("__progmune_public_surface__"))
1533
+ publicMarked.add(f.name);
1534
+ }
1535
+ const publicSurfacePropagated = process.env.PROGMUNE_PUBLIC_SURFACE_TOGGLE === "off"
1536
+ ? new Set()
1537
+ : (0, protocol_detector_1.computePublicSurfacePropagatedSet)(functions, publicMarked, guardPropagated, jobSurfacePropagated);
1515
1538
  for (const f of functions) {
1516
1539
  const fCalls = f.calls || [];
1517
1540
  let svs;
@@ -1526,8 +1549,11 @@ async function collectProtocolViolations(ctx, callGraph) {
1526
1549
  const params = (f.params || []).map((p) => ({ n: p.name, t: p.type || "" }));
1527
1550
  const directlyMarked = fCalls.includes("__progmune_auth_machinery__");
1528
1551
  const directlyJobMarked = fCalls.includes("__progmune_no_request_surface__");
1552
+ const directlyPublicMarked = fCalls.includes("__progmune_public_surface__");
1529
1553
  const guardExempt = !directlyMarked && guardPropagated.has(f.name);
1530
1554
  const jobExempt = !directlyJobMarked && jobSurfacePropagated.has(f.name);
1555
+ const mixedExempt = !directlyMarked && !directlyJobMarked && mixedSurfacePropagated.has(f.name);
1556
+ const publicExempt = !directlyPublicMarked && publicSurfacePropagated.has(f.name);
1531
1557
  for (const v of svs) {
1532
1558
  const alert = {
1533
1559
  rule: v.rule,
@@ -1547,6 +1573,14 @@ async function collectProtocolViolations(ctx, callGraph) {
1547
1573
  alert.suppressedByJobSurfacePropagation = true;
1548
1574
  suppressed = true;
1549
1575
  }
1576
+ if (mixedExempt) {
1577
+ alert.suppressedByMixedSurfacePropagation = true;
1578
+ suppressed = true;
1579
+ }
1580
+ if (!suppressed && publicExempt) {
1581
+ alert.suppressedByPublicSurfacePropagation = true;
1582
+ suppressed = true;
1583
+ }
1550
1584
  if (suppressed) {
1551
1585
  safeguardAlertsSuppressed.push(alert);
1552
1586
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.67",
3
+ "version": "3.7.69",
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/",