progmune-runtime 3.7.64 → 3.7.66

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,87 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.66] — 2026-10-12
4
+
5
+ ### 队列作业调用方传播(修冤枉第七刀:授权族告警的「只被作业调用的函数没有请求面」根因)
6
+
7
+ **根因**:第六刀(3.7.65)只豁免了作业处理器**自身**——docmost 残留
8
+ 告警中 service/repo 层函数只被 @Processor/@Interval 作业调用(鉴权
9
+ 发生在入队侧 controller,队列下游函数体不面向 HTTP 请求),授权族
10
+ 「缺鉴权/缺属主校验」的提问在它们身上同样不适用。
11
+
12
+ - **机制**:`computeJobSurfacePropagatedSet`(protocol-detector.ts,
13
+ 与第五刀镜像但判据不同)——**B 判据:调用方裸名并集**。凡调用名
14
+ 裸化后等于 F 裸名的函数都是「可能调用方」;并集非空且**全部**
15
+ 已豁免(种子 = 第六刀的 `__progmune_no_request_surface__` 标记)
16
+ ⇒ F 豁免。裸化只增候选(限定形态调用名也进并集)⇒ 真实调用方
17
+ 必在并集中(R75 正向证据)。**歧义下比第五刀更保守**:同名族
18
+ 任一成员被非作业调用,整个裸名族都不豁免(逐边消歧在作业链
19
+ Processor→XxxService 上恒失败——docmost 实测只剩同文件层 4 条)
20
+ - **快照轮次语义**,默认 1 轮(只豁免直接作业下游;传递闭包
21
+ docmost 实测 +16 条但豁免集 120→173,留作数据驱动再开);
22
+ `PROGMUNE_JOB_SURFACE_TOGGLE=off` 环境变量逃生门
23
+ - **标记而非静默丢弃(R9)**:被压告警进 `overall.safeguardAlerts.
24
+ suppressed` 清单(带 suppressedByJobSurfacePropagation 标记),
25
+ 不进 total/groups/topRanked 主计数;豁免仅作用于 authorization
26
+ 类、且不作用于自身直接带标记的函数(R5/R6)
27
+ - **同条件 A/B(docmost 双扫描,fresh IR)**:主告警流 720→697
28
+ (−23 = Unauthenticated Access 14 + Ownership Check 5 +
29
+ Unauthenticated Mutation 4),suppressed 117→140(+23 全带标记);
30
+ **主路径 violations 11=11 内容级零差 / score 84=84 / NEEDS_REVIEW
31
+ 不变**;control(toggle off)= 720/117 与 3.7.65 基线告警集逐位
32
+ 一致 ⇒ 压降完全归因本刀
33
+ - **23 条被压逐条核对**(blind-benchmark/verify-job-surface-66.ts):
34
+ 调用方并集全部为 @Processor 作业(NotificationProcessor.process /
35
+ onCompleted / handleFailedImportJob)或 @Interval 清理作业
36
+ (cleanupOldTrash / cleanupSessions),TP 0;同名族保守性独立复核
37
+ 通过(无任何被压函数存在非作业调用方)
38
+ - **回归锁定**:单元测试 11 例(protocol-detector-job-surface.test.ts:
39
+ 全作业传播/同名裸名多候选并集豁免(第五刀此处会阻断)/同名族保守/
40
+ 混合调用方 fail-safe/无调用方/1 轮边界/maxHops/自调无豁免证据/
41
+ 限定调用名裸化/种子语义/自定义标记)+ 引擎产品路径 2 例
42
+ (tests/trust/engine.test.ts:作业下游进 suppressed 不进主计数 +
43
+ HTTP 对照组照常上报);引擎套件 27/27;
44
+ taintpath 160 / webshape 72 / fr-007 5-0 / fr-016 7-0 维持;
45
+ TS 盲测 LOST 0 / ADDED 0(对 3.7.65 提交基线逐旗标一致;
46
+ batch-scan 不经过引擎告警循环,此门对本改动空过,
47
+ 真实门 = docmost A/B + 定向测试,如实记录)
48
+
49
+ ## [3.7.65] — 2026-10-11
50
+
51
+ ### 队列/定时/事件处理器无请求面豁免(修冤枉第六刀:授权族告警的「作业处理器不是请求面」根因)
52
+
53
+ **根因**:docmost 补标注里队列作业/定时任务类 FP 占授权族 FP 的
54
+ 10/111——鉴权发生在入队侧(controller 把任务塞进队列时已鉴权),
55
+ BullMQ 处理器 / NestJS Schedule / EventEmitter 监听器的函数体
56
+ 不面向 HTTP 请求,「缺鉴权/缺属主校验」的提问在处理器层不适用。
57
+
58
+ - **机制**:提取器注入 `__progmune_no_request_surface__` 标记——
59
+ 类级 `@Processor(队列名)`(BullMQ,该类全部方法)、方法级
60
+ `@Interval/@Cron/@Timeout/@OnEvent`(须带实参);规则侧仅授权族
61
+ 四条(Unauthenticated Access/Mutation + Ownership Check + Resource
62
+ Ownership)在 safeguard 位接受该标记(与 E2 同款「只做减法」)
63
+ - **反例挡住(是请求面,不注入)**:@Sse(HTTP SSE)、
64
+ @SubscribeMessage(WebSocket)、@MessagePattern/@EventPattern
65
+ (微服务 RPC);`@Processor()` 空实参不是 BullMQ 形态不注入
66
+ - **同条件 A/B(docmost 双扫描,fresh IR 各自重提)**:主告警流
67
+ 748→720(−28 = Ownership Check 10 + Unauthenticated Access 9 +
68
+ Unauthenticated Mutation 9),**主路径 violations 11=11 内容级零差 /
69
+ score 84=84 / suppressed 117=117**;注入关对照 = 748 与 3.7.64 时代
70
+ 基线一致 ⇒ 压降完全归因本刀,非扫描间抖动
71
+ - **可压名单交叉核对**:28 条被压告警逐条列函数/文件,全部命中
72
+ @Processor(FileTask/Notification/History/GeneralQueue ×4 类)或
73
+ @Interval(cleanupSessions/cleanupOldTrash/sendTelemetry)或
74
+ @OnEvent(Page/Space/Workspace Listener)形态,TP 0
75
+ - **回归锁定**:提取器测试 4 例(类级全方法/方法级四种/反例四种/
76
+ 空实参边界)+ 规则侧测试 5 例(四族抑制 + 非授权族不受影响);
77
+ TS 盲测 119 项目零漂移(LOST 0/ADDED 0——generated 语料无装饰器
78
+ 形状,此门对本改动是空过,真实门=docmost A/B + 定向测试,如实记录)
79
+ - **⚠ 过程教训(TSError 型 toggle)**:上一会话用 `return [];` 做
80
+ A/B 开关——unreachable 代码中 TS 控制流分析不执行收窄,`name`
81
+ 保持 `string | undefined` 编译失败,对照扫描 0 字节静默中断两轮。
82
+ toggle 必须用环境变量分支,且对照扫描前必须删 ir.json(IR-STALE
83
+ 复用含标记缓存会让「对照」跑在注入开的数据上)
84
+
3
85
  ## [3.7.64] — 2026-10-10
4
86
 
5
87
  ### 守卫沿调用链传播(修冤枉第五刀:授权族告警的「守卫在 controller 层」根因)
@@ -0,0 +1,142 @@
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-10):队列/定时/事件处理器 → 无请求面标记锁定。
43
+ *
44
+ * 数据:docmost 补标注 111 条授权族 FP 里 10 条是队列作业/定时任务——
45
+ * 鉴权发生在入队侧,处理器函数不面向 HTTP 请求,授权族规则按「请求面
46
+ * 缺鉴权」提问时结论不适用。
47
+ *
48
+ * 注入形态:@Processor(队列名) 类级(BullMQ,全方法);
49
+ * @Interval/@Cron/@Timeout/@OnEvent 方法级(须带实参)。
50
+ * 反例(是请求面,不注入):@Sse / @SubscribeMessage / @MessagePattern。
51
+ */
52
+ const MARK = "__progmune_no_request_surface__";
53
+ const TSCONFIG = JSON.stringify({
54
+ compilerOptions: { target: "ES2020", module: "commonjs", strict: false, skipLibCheck: true, noEmit: true },
55
+ include: ["**/*.ts"],
56
+ });
57
+ function makeProject(files) {
58
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-nosurface-"));
59
+ fs.writeFileSync(path.join(dir, "tsconfig.json"), TSCONFIG);
60
+ fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ name: "fixture", version: "0.0.0", private: true }));
61
+ for (const [rel, body] of Object.entries(files)) {
62
+ const abs = path.join(dir, rel);
63
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
64
+ fs.writeFileSync(abs, body);
65
+ }
66
+ return dir;
67
+ }
68
+ function callsOf(dir, fnName) {
69
+ const fns = (0, extract_ir_1.extractIR)(dir);
70
+ const f = fns.find((x) => x.name === fnName);
71
+ return f ? (f.calls ?? []) : [];
72
+ }
73
+ (0, vitest_1.describe)("extractIR:无请求面标记(修冤枉第六刀)", () => {
74
+ (0, vitest_1.it)("@Processor(队列名) 类级 ⇒ 全部方法注入", () => {
75
+ const dir = makeProject({
76
+ "a.ts": `
77
+ declare const Processor: any;
78
+ @Processor("notification-queue")
79
+ export class NotificationProcessor {
80
+ process() { this.markAsEmailed(); }
81
+ onCompleted() {}
82
+ }
83
+ `,
84
+ });
85
+ (0, vitest_1.expect)(callsOf(dir, "NotificationProcessor.process")).toContain(MARK);
86
+ (0, vitest_1.expect)(callsOf(dir, "NotificationProcessor.onCompleted")).toContain(MARK);
87
+ });
88
+ (0, vitest_1.it)("@Interval/@Cron/@Timeout/@OnEvent 方法级 ⇒ 仅该方法注入", () => {
89
+ const dir = makeProject({
90
+ "a.ts": `
91
+ declare const Interval: any; declare const Cron: any; declare const Timeout: any; declare const OnEvent: any;
92
+ export class SessionService {
93
+ @Interval("session-cleanup", 86400000)
94
+ cleanupSessions() {}
95
+ @Cron("0 0 * * *")
96
+ dailyJob() {}
97
+ @Timeout(5000)
98
+ delayedJob() {}
99
+ @OnEvent("user.created")
100
+ onUserCreated() {}
101
+ normalMethod() {}
102
+ }
103
+ `,
104
+ });
105
+ for (const fn of ["SessionService.cleanupSessions", "SessionService.dailyJob", "SessionService.delayedJob", "SessionService.onUserCreated"]) {
106
+ (0, vitest_1.expect)(callsOf(dir, fn)).toContain(MARK);
107
+ }
108
+ (0, vitest_1.expect)(callsOf(dir, "SessionService.normalMethod")).not.toContain(MARK);
109
+ });
110
+ (0, vitest_1.it)("反例:@Sse/@SubscribeMessage/@MessagePattern/@EventPattern 是请求面,不注入", () => {
111
+ const dir = makeProject({
112
+ "a.ts": `
113
+ declare const Sse: any; declare const SubscribeMessage: any; declare const MessagePattern: any; declare const EventPattern: any;
114
+ export class Gateway {
115
+ @Sse("events")
116
+ events() {}
117
+ @SubscribeMessage("chat")
118
+ onMessage() {}
119
+ @MessagePattern("orders.get")
120
+ getOrder() {}
121
+ @EventPattern("orders.created")
122
+ onOrderCreated() {}
123
+ }
124
+ `,
125
+ });
126
+ for (const fn of ["Gateway.events", "Gateway.onMessage", "Gateway.getOrder", "Gateway.onOrderCreated"]) {
127
+ (0, vitest_1.expect)(callsOf(dir, fn)).not.toContain(MARK);
128
+ }
129
+ });
130
+ (0, vitest_1.it)("边界:@Processor() 空实参不注入(非 BullMQ 形态)", () => {
131
+ const dir = makeProject({
132
+ "a.ts": `
133
+ declare const Processor: any;
134
+ @Processor()
135
+ export class Odd {
136
+ run() {}
137
+ }
138
+ `,
139
+ });
140
+ (0, vitest_1.expect)(callsOf(dir, "Odd.run")).not.toContain(MARK);
141
+ });
142
+ });
@@ -462,6 +462,44 @@ function authMachineryFromDecorators(decorators) {
462
462
  return auth && !skip ? [AUTH_MACHINERY_MARKER] : [];
463
463
  }
464
464
  // ═══════════════════════════════════════════════════════════════
465
+ // 修冤枉第六刀(2026-10-10):队列/定时/事件处理器 → 无请求面标记
466
+ //
467
+ // 数据:docmost 补标注 111 条授权族 FP 里 10 条是队列作业/定时任务——
468
+ // 鉴权发生在入队侧(controller 层),处理器函数本身不面向 HTTP 请求,
469
+ // 授权族规则按「请求面缺鉴权」提问时结论不适用。
470
+ // 当前流量化:44 个队列/定时函数命中 19 条授权族告警(全 FP 类)。
471
+ //
472
+ // 形态:@Processor(队列名) 类级(BullMQ,该类全部方法都是作业);
473
+ // @Interval/@Cron/@Timeout 方法级(NestJS Schedule);
474
+ // @OnEvent 方法级(NestJS EventEmitter2)。
475
+ // ⚠ 反例(是请求面,不注入):@Sse(HTTP SSE)、@SubscribeMessage
476
+ // (WebSocket 消息)、@MessagePattern/@EventPattern(微服务 RPC)。
477
+ // ═══════════════════════════════════════════════════════════════
478
+ const NO_REQUEST_SURFACE_MARKER = "__progmune_no_request_surface__";
479
+ /** 队列/定时/事件装饰器 —— 出现即注入无请求面标记(须带实参:@Interval('name', ms) 等) */
480
+ const NO_SURFACE_CLASS_DECORATOR = /^Processor$/i;
481
+ const NO_SURFACE_METHOD_DECORATOR = /^(?:Interval|Cron|Timeout|OnEvent)$/i;
482
+ /**
483
+ * 由类/方法装饰器判定是否注入无请求面标记(与 authMachineryFromDecorators 同款文本匹配)。
484
+ * @returns [] 或 [NO_REQUEST_SURFACE_MARKER]
485
+ */
486
+ function noRequestSurfaceFromDecorators(decorators) {
487
+ let noSurface = false;
488
+ for (const d of decorators) {
489
+ const text = (d?.getText?.() ?? "").trim();
490
+ const name = /^@([A-Za-z_$][\w$]*)/.exec(text)?.[1];
491
+ if (!name)
492
+ continue;
493
+ if (!NO_SURFACE_CLASS_DECORATOR.test(name) && !NO_SURFACE_METHOD_DECORATOR.test(name))
494
+ continue;
495
+ // 须带实参(@Processor() 空参不是 BullMQ 形态;@Interval 必带名字+毫秒)
496
+ if (!/^@[\w$]*\([^)]+\)/.test(text))
497
+ continue;
498
+ noSurface = true;
499
+ }
500
+ return noSurface ? [NO_REQUEST_SURFACE_MARKER] : [];
501
+ }
502
+ // ═══════════════════════════════════════════════════════════════
465
503
  // E3(2026-09-22):TS 侧「入参已按 schema 校验」语义标记
466
504
  //
467
505
  // 判据(先抄准再写 expect,R22):
@@ -3256,6 +3294,15 @@ function _extractSingleProject(absRoot, tsconfigPath) {
3256
3294
  if (!mCalls.includes(mk))
3257
3295
  mCalls.push(mk);
3258
3296
  }
3297
+ // 修冤枉第六刀:队列/定时/事件处理器 → 无请求面标记(与 E2 并列,
3298
+ // 语义相反——E2 是「有鉴权」,本标记是「鉴权问题不适用」)
3299
+ for (const mk of noRequestSurfaceFromDecorators([
3300
+ ...(cls.getDecorators?.() ?? []),
3301
+ ...(m.getDecorators?.() ?? []),
3302
+ ])) {
3303
+ if (!mCalls.includes(mk))
3304
+ mCalls.push(mk);
3305
+ }
3259
3306
  funcs.push({
3260
3307
  name: `${cn}.${mn}`,
3261
3308
  params: mParams.map((p) => ({ name: p.getName(), type: getParamType(p), typeDetail: getParamTypeDetail(p) })),
@@ -0,0 +1,96 @@
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-11)单元测试。
7
+ *
8
+ * B 判据(与第五刀逐边消歧镜像,但判据不同):调用方**裸名并集**——
9
+ * 凡调用名等于 F 裸名的函数都是「可能调用方」;并集非空且全部已豁免
10
+ * (种子 = 带 __progmune_no_request_surface__ 标记的作业处理器)
11
+ * ⇒ F 豁免。歧义下比逐边消歧更保守:同名族任一成员被非作业调用,
12
+ * 整个裸名族都不豁免。快照轮次语义;默认 1 轮。
13
+ * 消费方(engine)负责 R5/R6:只压 authorization 类、且不压直接标记函数。
14
+ */
15
+ const M = "__progmune_no_request_surface__";
16
+ (0, vitest_1.describe)("computeJobSurfacePropagatedSet(作业调用方传播 B 判据)", () => {
17
+ (0, vitest_1.it)("全部调用方都是作业 ⇒ 传播(processor→service)", () => {
18
+ const fns = [
19
+ { name: "NotificationProcessor.process", file: "q.ts", calls: [M, "processPageUpdate"] },
20
+ { name: "PageNotificationService.processPageUpdate", file: "s.ts", calls: ["insertLog"] },
21
+ ];
22
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
23
+ (0, vitest_1.expect)(g.has("PageNotificationService.processPageUpdate")).toBe(true);
24
+ });
25
+ (0, vitest_1.it)("调用方含 HTTP 面 ⇒ 不豁免(fail-safe)", () => {
26
+ const fns = [
27
+ { name: "NotificationProcessor.process", file: "q.ts", calls: [M, "processPageUpdate"] },
28
+ { name: "PageController.getPage", file: "c.ts", calls: ["processPageUpdate"] },
29
+ { name: "PageNotificationService.processPageUpdate", file: "s.ts", calls: [] },
30
+ ];
31
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
32
+ (0, vitest_1.expect)(g.has("PageNotificationService.processPageUpdate")).toBe(false);
33
+ });
34
+ (0, vitest_1.it)("无调用方(入口函数)⇒ 不豁免", () => {
35
+ const fns = [{ name: "bootstrap", file: "m.ts", calls: [] }];
36
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
37
+ (0, vitest_1.expect)(g.has("bootstrap")).toBe(false);
38
+ });
39
+ (0, vitest_1.it)("默认 1 轮:只豁免作业直接下游,不传递闭包", () => {
40
+ const fns = [
41
+ { name: "Job.run", file: "q.ts", calls: [M, "step1"] },
42
+ { name: "Svc.step1", file: "s.ts", calls: ["step2"] },
43
+ { name: "Repo.step2", file: "r.ts", calls: [] },
44
+ ];
45
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
46
+ (0, vitest_1.expect)(g.has("Svc.step1")).toBe(true);
47
+ (0, vitest_1.expect)(g.has("Repo.step2")).toBe(false);
48
+ });
49
+ (0, vitest_1.it)("maxHops=2 时传递两跳", () => {
50
+ const fns = [
51
+ { name: "Job.run", file: "q.ts", calls: [M, "step1"] },
52
+ { name: "Svc.step1", file: "s.ts", calls: ["step2"] },
53
+ { name: "Repo.step2", file: "r.ts", calls: [] },
54
+ ];
55
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns, { maxHops: 2 });
56
+ (0, vitest_1.expect)(g.has("Svc.step1")).toBe(true);
57
+ (0, vitest_1.expect)(g.has("Repo.step2")).toBe(true);
58
+ });
59
+ (0, vitest_1.it)("同名族保守:任一成员被非作业调用,整个裸名族都不豁免", () => {
60
+ const fns = [
61
+ { name: "Job.run", file: "q.ts", calls: [M, "create"] },
62
+ { name: "PageService.create", file: "s.ts", calls: [] },
63
+ { name: "CommentService.create", file: "c2.ts", calls: [] },
64
+ { name: "PageController.post", file: "c.ts", calls: ["create"] },
65
+ ];
66
+ // create 裸名族含 CommentService.create——PageController.post(非作业)
67
+ // 也在调用方并集中 ⇒ 族级不豁免,即使 Job.run 只调 PageService.create
68
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
69
+ (0, vitest_1.expect)(g.has("PageService.create")).toBe(false);
70
+ (0, vitest_1.expect)(g.has("CommentService.create")).toBe(false);
71
+ });
72
+ (0, vitest_1.it)("限定形态调用名(Class.method)按裸名命中", () => {
73
+ const fns = [
74
+ { name: "Job.run", file: "q.ts", calls: [M, "PageService.processUpdate"] },
75
+ { name: "PageService.processUpdate", file: "s.ts", calls: [] },
76
+ ];
77
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
78
+ (0, vitest_1.expect)(g.has("PageService.processUpdate")).toBe(true);
79
+ });
80
+ (0, vitest_1.it)("直接标记函数在返回集合中(种子语义,R6 由消费方过滤)", () => {
81
+ const fns = [{ name: "Job.run", file: "q.ts", calls: [M] }];
82
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
83
+ (0, vitest_1.expect)(g.has("Job.run")).toBe(true);
84
+ });
85
+ (0, vitest_1.it)("自定义标记通道", () => {
86
+ const fns = [
87
+ { name: "A", file: "f.ts", calls: ["__custom_mark__", "B"] },
88
+ { name: "B", file: "f.ts", calls: [] },
89
+ ];
90
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns, { marker: "__custom_mark__" });
91
+ (0, vitest_1.expect)(g.has("B")).toBe(true);
92
+ // 默认标记下不传播
93
+ const g2 = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
94
+ (0, vitest_1.expect)(g2.has("B")).toBe(false);
95
+ });
96
+ });
@@ -0,0 +1,118 @@
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-11)单元测试。
7
+ *
8
+ * B 判据(裸名并集,见 computeJobSurfacePropagatedSet 注释):
9
+ * 函数的调用方并集(调用名=裸名的所有函数)非空且全部带
10
+ * __progmune_no_request_surface__ 标记 ⇒ 豁免;默认 1 轮。
11
+ * 正确性前提:IR 调用名即裸名 ⇒ 真实调用方必在并集中 ⇒
12
+ * 并集全作业蕴含真实调用方全作业(R75 正向证据)。
13
+ * 与第五刀的差别:歧义不阻断——同名族任一成员被非作业调用,
14
+ * 整个裸名族都不豁免(比逐边消歧更保守的族级行为)。
15
+ * 消费方(engine)负责 R5/R6:只压 authorization 类、且不压直接标记函数。
16
+ */
17
+ const M = "__progmune_no_request_surface__";
18
+ (0, vitest_1.describe)("computeJobSurfacePropagatedSet(作业调用方传播,B 判据)", () => {
19
+ (0, vitest_1.it)("全部调用方是作业 ⇒ 传播(Processor→Service)", () => {
20
+ const fns = [
21
+ { name: "NotificationProcessor.process", file: "q/notification.processor.ts", calls: [M, "processPageUpdate"] },
22
+ { name: "PageNotificationService.processPageUpdate", file: "s/page-notification.service.ts", calls: ["insertNotification"] },
23
+ ];
24
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
25
+ (0, vitest_1.expect)(g.has("PageNotificationService.processPageUpdate")).toBe(true);
26
+ });
27
+ (0, vitest_1.it)("同名裸名多候选且调用方全作业 ⇒ 并集判据豁免(第五刀此处会阻断)", () => {
28
+ // processDigest 裸名有两个定义;调用方是两个不同的作业
29
+ // 处理器。逐边消歧(第五刀)会挂未解析边阻断;并集判据
30
+ // 检查「所有可能调用方全作业」⇒ 豁免。
31
+ const fns = [
32
+ { name: "EmailProcessor.process", file: "q/email.processor.ts", calls: [M, "processDigest"] },
33
+ { name: "DigestProcessor.process", file: "q/digest.processor.ts", calls: [M, "processDigest"] },
34
+ { name: "PageNotificationService.processDigest", file: "s/page.service.ts", calls: [] },
35
+ { name: "CommentNotificationService.processDigest", file: "s/comment.service.ts", calls: [] },
36
+ ];
37
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
38
+ (0, vitest_1.expect)(g.has("PageNotificationService.processDigest")).toBe(true);
39
+ (0, vitest_1.expect)(g.has("CommentNotificationService.processDigest")).toBe(true);
40
+ });
41
+ (0, vitest_1.it)("同名族任一成员被非作业调用 ⇒ 整个裸名族不豁免(保守族级)", () => {
42
+ const fns = [
43
+ { name: "EmailProcessor.process", file: "q.ts", calls: [M, "processDigest"] },
44
+ { name: "PageController.show", file: "c.ts", calls: ["processDigest"] },
45
+ { name: "PageNotificationService.processDigest", file: "s1.ts", calls: [] },
46
+ { name: "CommentNotificationService.processDigest", file: "s2.ts", calls: [] },
47
+ ];
48
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
49
+ (0, vitest_1.expect)(g.has("PageNotificationService.processDigest")).toBe(false);
50
+ (0, vitest_1.expect)(g.has("CommentNotificationService.processDigest")).toBe(false);
51
+ });
52
+ (0, vitest_1.it)("调用方含非作业 ⇒ 不豁免(fail-safe)", () => {
53
+ const fns = [
54
+ { name: "EmailProcessor.process", file: "q.ts", calls: [M, "markAsEmailed"] },
55
+ { name: "NotificationService.markAsEmailedDirect", file: "s.ts", calls: ["markAsEmailed"] },
56
+ { name: "NotificationRepo.markAsEmailed", file: "r.ts", calls: [] },
57
+ ];
58
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
59
+ (0, vitest_1.expect)(g.has("NotificationRepo.markAsEmailed")).toBe(false);
60
+ });
61
+ (0, vitest_1.it)("无调用方(入口/未连通)⇒ 不豁免", () => {
62
+ const fns = [
63
+ { name: "orphanJobHelper", file: "m.ts", calls: [] },
64
+ ];
65
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
66
+ (0, vitest_1.expect)(g.has("orphanJobHelper")).toBe(false);
67
+ });
68
+ (0, vitest_1.it)("默认 1 轮:作业下游的下游不豁免(传递闭包未开)", () => {
69
+ const fns = [
70
+ { name: "NotificationProcessor.process", file: "q.ts", calls: [M, "processPageUpdate"] },
71
+ { name: "PageNotificationService.processPageUpdate", file: "s.ts", calls: ["insertNotification"] },
72
+ { name: "NotificationRepo.insertNotification", file: "r.ts", calls: [] },
73
+ ];
74
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
75
+ (0, vitest_1.expect)(g.has("PageNotificationService.processPageUpdate")).toBe(true);
76
+ (0, vitest_1.expect)(g.has("NotificationRepo.insertNotification")).toBe(false);
77
+ });
78
+ (0, vitest_1.it)("maxHops=2 ⇒ 传递两跳成立(快照轮次语义)", () => {
79
+ const fns = [
80
+ { name: "NotificationProcessor.process", file: "q.ts", calls: [M, "processPageUpdate"] },
81
+ { name: "PageNotificationService.processPageUpdate", file: "s.ts", calls: ["insertNotification"] },
82
+ { name: "NotificationRepo.insertNotification", file: "r.ts", calls: [] },
83
+ ];
84
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns, { maxHops: 2 });
85
+ (0, vitest_1.expect)(g.has("NotificationRepo.insertNotification")).toBe(true);
86
+ });
87
+ (0, vitest_1.it)("自调不提供豁免证据(自己不是作业 ⇒ 不能靠自调豁免)", () => {
88
+ const fns = [
89
+ { name: "CleanupService.cleanup", file: "s.ts", calls: ["cleanup"] },
90
+ ];
91
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
92
+ (0, vitest_1.expect)(g.has("CleanupService.cleanup")).toBe(false);
93
+ });
94
+ (0, vitest_1.it)("限定形态调用名(Class.method)按裸名命中(裸化只增候选,保守方向)", () => {
95
+ const fns = [
96
+ { name: "Job.run", file: "q.ts", calls: [M, "PageService.processUpdate"] },
97
+ { name: "PageService.processUpdate", file: "s.ts", calls: [] },
98
+ ];
99
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
100
+ (0, vitest_1.expect)(g.has("PageService.processUpdate")).toBe(true);
101
+ });
102
+ (0, vitest_1.it)("直接标记函数在返回集合中(种子语义,R6 由消费方过滤)", () => {
103
+ const fns = [{ name: "Job.run", file: "q.ts", calls: [M] }];
104
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
105
+ (0, vitest_1.expect)(g.has("Job.run")).toBe(true);
106
+ });
107
+ (0, vitest_1.it)("自定义标记通道", () => {
108
+ const fns = [
109
+ { name: "A", file: "f.ts", calls: ["__custom_mark__", "B"] },
110
+ { name: "B", file: "f.ts", calls: [] },
111
+ ];
112
+ const g = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns, { marker: "__custom_mark__" });
113
+ (0, vitest_1.expect)(g.has("B")).toBe(true);
114
+ // 默认标记下不传播
115
+ const g2 = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
116
+ (0, vitest_1.expect)(g2.has("B")).toBe(false);
117
+ });
118
+ });
@@ -0,0 +1,44 @@
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-10):无请求面标记的规则侧接受位锁定。
7
+ *
8
+ * 语义:队列/定时/事件处理器不面向 HTTP 请求,鉴权发生在入队侧——
9
+ * 授权族四条的「缺鉴权/缺属主校验」提问在处理器层不适用(N/A 抑制)。
10
+ * 只抑制 authorization 类;其他族规则不受标记影响。
11
+ */
12
+ const M = "__progmune_no_request_surface__";
13
+ const has = (vs, rule) => vs.some((v) => v.rule === rule);
14
+ (0, vitest_1.describe)("no_request_surface 标记:授权族四条接受、其他族不受影响", () => {
15
+ const calls = ["getWorkspaceUrl", M];
16
+ (0, vitest_1.it)("Unauthenticated Access 被抑制", () => {
17
+ const bare = (0, protocol_detector_1.detectSafeguardViolations)(["getWorkspaceUrl"], "NotificationProcessor.getWorkspaceUrl", "typescript", ["job"], true);
18
+ (0, vitest_1.expect)(has(bare, "Authorization (Unauthenticated Access)")).toBe(true);
19
+ const withMark = (0, protocol_detector_1.detectSafeguardViolations)(calls, "NotificationProcessor.getWorkspaceUrl", "typescript", ["job"], true);
20
+ (0, vitest_1.expect)(has(withMark, "Authorization (Unauthenticated Access)")).toBe(false);
21
+ });
22
+ (0, vitest_1.it)("Unauthenticated Mutation 被抑制", () => {
23
+ const bare = (0, protocol_detector_1.detectSafeguardViolations)(["updateTable"], "HistoryProcessor.process", "typescript", ["job"], true);
24
+ (0, vitest_1.expect)(has(bare, "Authorization (Unauthenticated Mutation)")).toBe(true);
25
+ const withMark = (0, protocol_detector_1.detectSafeguardViolations)(["updateTable", M], "HistoryProcessor.process", "typescript", ["job"], true);
26
+ (0, vitest_1.expect)(has(withMark, "Authorization (Unauthenticated Mutation)")).toBe(false);
27
+ });
28
+ (0, vitest_1.it)("Ownership Check 被抑制(queue job 的属主校验提问不适用)", () => {
29
+ const bare = (0, protocol_detector_1.detectSafeguardViolations)(["deleteSessions"], "SessionService.cleanupSessions", "typescript", ["job"], true);
30
+ (0, vitest_1.expect)(has(bare, "Authorization (Ownership Check)")).toBe(true);
31
+ const withMark = (0, protocol_detector_1.detectSafeguardViolations)(["deleteSessions", M], "SessionService.cleanupSessions", "typescript", ["job"], true);
32
+ (0, vitest_1.expect)(has(withMark, "Authorization (Ownership Check)")).toBe(false);
33
+ });
34
+ (0, vitest_1.it)("Resource Ownership 被抑制", () => {
35
+ const bare = (0, protocol_detector_1.detectSafeguardViolations)(["removeIcon"], "AttachmentProcessor.process", "typescript", ["job"], true);
36
+ (0, vitest_1.expect)(has(bare, "Authorization (Resource Ownership)")).toBe(true);
37
+ const withMark = (0, protocol_detector_1.detectSafeguardViolations)(["removeIcon", M], "AttachmentProcessor.process", "typescript", ["job"], true);
38
+ (0, vitest_1.expect)(has(withMark, "Authorization (Resource Ownership)")).toBe(false);
39
+ });
40
+ (0, vitest_1.it)("非授权族规则不受标记影响(渲染 sink 照报)", () => {
41
+ const vs = (0, protocol_detector_1.detectSafeguardViolations)(["render", M], "EmailProcessor.renderTemplate", "typescript");
42
+ (0, vitest_1.expect)(has(vs, "No Input Sanitization")).toBe(true);
43
+ });
44
+ });
@@ -15,6 +15,7 @@ exports.classifyParamType = classifyParamType;
15
15
  exports.detectSafeguardViolations = detectSafeguardViolations;
16
16
  exports.buildCallerMap = buildCallerMap;
17
17
  exports.computeGuardPropagatedSet = computeGuardPropagatedSet;
18
+ exports.computeJobSurfacePropagatedSet = computeJobSurfacePropagatedSet;
18
19
  exports.detectSafeguardViolationsV7 = detectSafeguardViolationsV7;
19
20
  exports.detectProtocolViolations = detectProtocolViolations;
20
21
  exports.validateProtocolState = validateProtocolState;
@@ -385,6 +386,9 @@ const SAFEGUARD_RULES = [
385
386
  safeguards: [
386
387
  { pattern: /\b(checkOwner|isOwner|ownerId\s*[!=]==?|authorId\s*[!=]==?|userId\s*[!=]==?|createdBy\s*[!=]==?|\.owner\s*[!=]==?|\.user\s*[!=]==?)\b/i, label: "ownership_check" },
387
388
  { pattern: /\b(hasPermission|checkPermission|checkAccess|isAuthorized|checkRole|requireRole|adminCheck|isAdmin|canModify|canDelete|canEdit)\b/i, label: "authz_check" },
389
+ // 修冤枉第六刀:队列/定时/事件处理器不面向请求面——鉴权发生在入队侧,
390
+ // 「改资源没查属主」的提问在处理器层不适用(docmost 补标注 FP 类)
391
+ { pattern: /\b(__progmune_no_request_surface__)\b/, label: "no_request_surface" },
388
392
  { pattern: /\b(__progmune_ownership_checked__)\b/, label: "inline_ownership_check" },
389
393
  // §46 ②:资源属主被**赋值为当前主体**(`{ userId: user.id }` / `where:{userId: user.id}`)。
390
394
  // 这类函数的被操作对象就是(或已被限定在)主体自己,谈不上「改别人的东西」。
@@ -428,7 +432,7 @@ const SAFEGUARD_RULES = [
428
432
  paramTypeSafeguards: [SESSION_BEARING_TYPE_RE],
429
433
  trigger: /\b(list|download|view|fetch)(?:[A-Z]\w*|_\w+)|get(?:[A-Z]\w+|_\w+)/i,
430
434
  safeguards: [
431
- { 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__)\b/i, label: "auth_check" },
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" },
432
436
  // §40 C1:裸 can —— verdaccio 的 can('publish') 中间件就是这里的授权判定
433
437
  { pattern: exports.AUTHZ_BARE_CAN_RE, label: "bare_can", callsOnly: true },
434
438
  ],
@@ -464,7 +468,7 @@ const SAFEGUARD_RULES = [
464
468
  // (listPosts/getPost/deletePost fire via identifier-parsed words).
465
469
  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,
466
470
  safeguards: [
467
- { 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__)\b/i, label: "auth_check" },
471
+ { 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" },
468
472
  // §40 C1:裸 can —— 同上
469
473
  { pattern: exports.AUTHZ_BARE_CAN_RE, label: "bare_can", callsOnly: true },
470
474
  // §46 ①:凭签发入口豁免 —— 独立 safeguard,**不并入 auth_check**。
@@ -624,6 +628,8 @@ const SAFEGUARD_RULES = [
624
628
  safeguards: [
625
629
  { pattern: /\b(ownerId\s*[!=]==?|authorId\s*[!=]==?|userId\s*[!=]==?|createdBy|\.owner\s*[!=]==?)/i, label: "ownership_comparison" },
626
630
  { pattern: /\b(__progmune_ownership_checked__)\b/, label: "inline_ownership_check" },
631
+ // 修冤枉第六刀:同上(无请求面 ⇒ 属主校验提问不适用)
632
+ { pattern: /\b(__progmune_no_request_surface__)\b/, label: "no_request_surface" },
627
633
  // §38:同 Ownership Check,组合式谓语一并接上(这条规则更严,更需要)
628
634
  { pattern: exports.AUTHZ_PREDICATE_RE, label: "casl_predicate" },
629
635
  { pattern: exports.AUTHZ_CAN_HELPER_RE, label: "can_helper" },
@@ -1748,6 +1754,67 @@ function computeGuardPropagatedSet(funcs, opts) {
1748
1754
  }
1749
1755
  return guarded;
1750
1756
  }
1757
+ /**
1758
+ * 修冤枉第七刀(2026-10-11):队列作业调用方传播 ——「函数只被作业调用
1759
+ * ⇒ 无请求面」。与 computeGuardPropagatedSet(第五刀)镜像,但判据不同:
1760
+ *
1761
+ * 第五刀对被调名做逐边分层消歧(后缀唯一→同文件→同目录→类根),歧义
1762
+ * fail-safe 挂未解析边 ⇒ 任何歧义阻断豁免。而作业链(Processor→XxxService)
1763
+ * 跨目录、类根不同名,分层消歧恒失败——docmost 实测第七刀若沿用该框架
1764
+ * 只剩同文件层 4 条(预测压掉 4/600)。
1765
+ *
1766
+ * 本判据(B 判据):调用方**裸名并集**——凡调用名裸化后等于 F 裸名的函数
1767
+ * 都是「可能调用方」;并集非空且全部已豁免 ⇒ F 豁免。正确性:裸化只增
1768
+ * 候选(限定形态调用名也被收进并集)⇒ 真实调用方必在并集中 ⇒ 并集全作业
1769
+ * 蕴含真实调用方全作业(R75 正向证据)。歧义下比逐边消歧更保守:同名族
1770
+ * 任一成员被非作业调用,整个裸名族都不豁免。
1771
+ * 快照轮次语义与第五刀一致;默认 1 轮(只豁免直接作业下游;传递闭包
1772
+ * docmost 实测 +16 条告警命中但豁免集从 120 膨胀到 173,留作数据驱动再开)。
1773
+ */
1774
+ function computeJobSurfacePropagatedSet(funcs, opts) {
1775
+ const marker = opts?.marker ?? "__progmune_no_request_surface__";
1776
+ const maxHops = opts?.maxHops ?? 1;
1777
+ const bySuffix = new Map();
1778
+ for (const f of funcs) {
1779
+ const bare = f.name.split(".").pop() || f.name;
1780
+ if (!bySuffix.has(bare))
1781
+ bySuffix.set(bare, []);
1782
+ bySuffix.get(bare).push(f.name);
1783
+ }
1784
+ const callersByBare = new Map();
1785
+ for (const f of funcs) {
1786
+ for (const c of f.calls || []) {
1787
+ const bareC = c.split(".").pop() || c; // 裸化:限定形态调用名也进并集(只增候选,保守方向)
1788
+ if (!bySuffix.has(bareC))
1789
+ continue; // 非项目函数名 → 无豁免语义
1790
+ if (!callersByBare.has(bareC))
1791
+ callersByBare.set(bareC, new Set());
1792
+ callersByBare.get(bareC).add(f.name);
1793
+ }
1794
+ }
1795
+ const exempt = new Set();
1796
+ for (const f of funcs)
1797
+ if ((f.calls || []).includes(marker))
1798
+ exempt.add(f.name);
1799
+ for (let round = 0; round < maxHops; round++) {
1800
+ const toAdd = [];
1801
+ for (const f of funcs) {
1802
+ if (exempt.has(f.name))
1803
+ continue;
1804
+ const bare = f.name.split(".").pop() || f.name;
1805
+ const callers = callersByBare.get(bare);
1806
+ if (!callers || callers.size === 0)
1807
+ continue; // 无调用方 → 不豁免
1808
+ if ([...callers].every((c) => exempt.has(c)))
1809
+ toAdd.push(f.name);
1810
+ }
1811
+ if (toAdd.length === 0)
1812
+ break;
1813
+ for (const n of toAdd)
1814
+ exempt.add(n);
1815
+ }
1816
+ return exempt;
1817
+ }
1751
1818
  /**
1752
1819
  * v7: Build safeguard context — own → same-file → caller chain.
1753
1820
  *
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.64";
23
+ exports.RUNTIME_VERSION = "3.7.66";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
@@ -1504,6 +1504,14 @@ async function collectProtocolViolations(ctx, callGraph) {
1504
1504
  // ≤3 跳、歧义 fail-safe。豁免仅作用于 authorization 类规则
1505
1505
  // 且函数自身不直接带标记(R5/R6)——标记而非静默丢弃。
1506
1506
  const guardPropagated = (0, protocol_detector_1.computeGuardPropagatedSet)(functions);
1507
+ // 修冤枉第七刀(2026-10-11):队列作业调用方传播——「函数只被
1508
+ // 作业/定时/事件处理器调用 ⇒ 无请求面」。B 判据(裸名并集,见
1509
+ // computeJobSurfacePropagatedSet):并集非空且全部已豁免才豁免,
1510
+ // 默认 1 轮。豁免仅作用于 authorization 类且函数自身不直接带
1511
+ // 标记(R5/R6)——与第五刀同:标记而非静默丢弃。
1512
+ const jobSurfacePropagated = process.env.PROGMUNE_JOB_SURFACE_TOGGLE === "off"
1513
+ ? new Set()
1514
+ : (0, protocol_detector_1.computeJobSurfacePropagatedSet)(functions);
1507
1515
  for (const f of functions) {
1508
1516
  const fCalls = f.calls || [];
1509
1517
  let svs;
@@ -1517,7 +1525,9 @@ async function collectProtocolViolations(ctx, callGraph) {
1517
1525
  continue;
1518
1526
  const params = (f.params || []).map((p) => ({ n: p.name, t: p.type || "" }));
1519
1527
  const directlyMarked = fCalls.includes("__progmune_auth_machinery__");
1528
+ const directlyJobMarked = fCalls.includes("__progmune_no_request_surface__");
1520
1529
  const guardExempt = !directlyMarked && guardPropagated.has(f.name);
1530
+ const jobExempt = !directlyJobMarked && jobSurfacePropagated.has(f.name);
1521
1531
  for (const v of svs) {
1522
1532
  const alert = {
1523
1533
  rule: v.rule,
@@ -1527,13 +1537,22 @@ async function collectProtocolViolations(ctx, callGraph) {
1527
1537
  nRules: svs.length,
1528
1538
  params,
1529
1539
  };
1530
- if (guardExempt && v.category === "authorization") {
1531
- alert.suppressedByGuardPropagation = true;
1532
- safeguardAlertsSuppressed.push(alert);
1533
- }
1534
- else {
1535
- safeguardAlertsRaw.push(alert);
1540
+ if (v.category === "authorization") {
1541
+ let suppressed = false;
1542
+ if (guardExempt) {
1543
+ alert.suppressedByGuardPropagation = true;
1544
+ suppressed = true;
1545
+ }
1546
+ if (jobExempt) {
1547
+ alert.suppressedByJobSurfacePropagation = true;
1548
+ suppressed = true;
1549
+ }
1550
+ if (suppressed) {
1551
+ safeguardAlertsSuppressed.push(alert);
1552
+ continue;
1553
+ }
1536
1554
  }
1555
+ safeguardAlertsRaw.push(alert);
1537
1556
  }
1538
1557
  }
1539
1558
  safeguardAlerts = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.64",
3
+ "version": "3.7.66",
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/",