progmune-runtime 3.7.48 → 3.7.51

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.51] — 2026-09-22
4
+
5
+ ### Input Validation 的 trigger 只认函数名:不再被 callee 拖下水(F 轮)
6
+
7
+ - **缺口(不是词表问题,是「陈述对象」错位)**:本规则的 violationMessage 是
8
+ *"Content creation function does not validate…"* —— 陈述对象是
9
+ **「这个函数自己是不是内容创建函数」**。但 trigger 此前拿 **callee 名字**
10
+ 去匹配 `create*|add*|post*|upload*`,于是产生两类缺陷:
11
+ 1. **重复投射**:分发器 `handleRequest` 因调用 `createEvent` / `createRoute`
12
+ 被计一次,而那些 callee 自己也在报 ⇒ 同一条问题计两遍;
13
+ 2. **理由错**:`login` / `registerNewUser` / `authenticate` 因调用
14
+ **`createHash`**(密码哈希)被判成「内容创建函数未校验输入」。
15
+ - **修复**:新增规则字段 `triggerOwnNameOnly`(只拿 enclosing function 自身名字
16
+ 去匹配 trigger),在 `detectSafeguardViolations` 里加分支,并在
17
+ Input Validation 规则上开启。其余规则行为不变。
18
+ - **175 条损失逐条归类,无一真阳性**(R30):100 条重复投射(callee 自己仍报)、
19
+ 69 条 `createHash`、6 条 `list/get` 查询函数。真实池 37 条同理
20
+ (7 重复投射 / 3 被其他规则覆盖 / 27 触发源为 ORM·哈希·权限工厂·内部工具)。
21
+ - **实测**:TS 盲测 Input Validation **417 → 242(−175)**、ADDED 0、
22
+ **其他规则 0 变动**;FP 池 docmost 24 → 14(−10)、ADDED 0。
23
+ 离线重放(R29)的预测与真实重扫**逐条一致**(第二次确认保真度)。
24
+ - **已知边界(不掩饰)**:函数名不含 create/add/post/upload、但确实是
25
+ 「入口把用户输入交给内容创建」的函数(如 `handleSubmit` 调
26
+ `commentRepo.create`,而 create 不在扫描范围内)本轮后不再报。175 条里此类
27
+ 为 0;若要补回需要 `exposed`(web handler)通道 —— **按 R27 先查通道**。
28
+
29
+ ## [3.7.50] — 2026-09-22
30
+
31
+ ### TS 侧「入参已按 schema 校验」语义标记:DTO 的校验第一次对规则可见(E3)
32
+
33
+ - **缺口(R27 说的「证据通道断着」的实例)**:Input Validation 的 safeguard 是
34
+ **纯词形匹配,不接受任何 `__progmune_*` 标记**(`protocol-detector.ts:481`)。
35
+ NestJS 把校验写在**另一个文件**的 DTO 类属性装饰器上(`@IsString()` / `@MinLength(2)`),
36
+ 既不在函数体文本里、也不在 `calls` 里 ⇒ 规则永远看不到。E2 结束时已实测确认
37
+ 「装饰器救不了 Input Validation」,本轮是**接通道**,不是调词表。
38
+ - **修复(双侧,各一行)**:
39
+ - 提取器 `src/extract-ir.ts`:新增 `validatedDtoClassNames(project)` 预扫
40
+ (类自身或其属性上带校验装饰器 ⇒ 类名入集合),主循环后统一应用 ——
41
+ 入参类型是集合中的类 ⇒ 注入 `__progmune_input_schema__`。
42
+ 按**入参**判定、不按函数形态 ⇒ 函数声明 / 箭头 / 类方法全形态覆盖。
43
+ - 规则 `protocol-detector.ts`:Input Validation 的 safeguard 追加一个接受分支。
44
+ **只加在抑制位,不进 trigger**——新通道只能做减法。
45
+ - **标记命名是一个真陷阱(本轮抓到的)**:初版取名 `__progmune_validated_input__`,
46
+ 离线重放发现它会被 `Data Integrity (Foreign Key)` 的 safeguard
47
+ `\b(…|validate|…)(?:[A-Z]\w*|_\w+)\b` 命中(`validate` + `d_input__`),
48
+ 凭空压掉 **10 条无关违规**。改名 `__progmune_input_schema__`(不含
49
+ get/find/check/verify/validate/create/add/post/upload 任何动词词根)后归零。
50
+ 已加定向用例守住这条。
51
+ - **实测效果**(FP 观测池,docmost 切片真实重扫):Input Validation **39 → 24(−15)**,
52
+ **新增 0**,其他规则 **0 变动**。全池口径 −17(含 lujakob −1、w3tecch −1)。
53
+ - 回归:tsc ✓ / taintpath 160 条 ✓ / fr-007 pre 5·post 0、fr-016 pre 7·post 0 ✓ /
54
+ 定向测试 **187 全绿**(新增 E3 组 11 条)/ 反向验证两刀(拆规则侧接受位、关提取器
55
+ 发射)各自精确回到 39、残留 0。
56
+ - **TS 盲测 3201 → 3201、逐函数差异 0 —— 这是空过(R23 家族第四次)**,已核实:
57
+ generated 语料里校验装饰器命中数 **0**,该门测不到本轮;真证据来自 FP 观测池。
58
+ - 另附:本轮先用**离线重放**(拿已记录的 `calls + name` 直接喂 detector,不重新提取 IR)
59
+ 预测量三个候选变体的效果,再挑一个实现。重放预测的 −15 与真实重扫**逐条一致**。
60
+
61
+ ## [3.7.49] — 2026-09-22
62
+
63
+ ### TS 侧「框架鉴权」语义标记:装饰器里的鉴权第一次对规则可见(E2)
64
+
65
+ - **缺口**:NestJS 的鉴权写在装饰器里(`@UseGuards(JwtAuthGuard)`,类级或方法级),
66
+ 既不在函数体文本里、也不在 `calls` 里 ⇒ 规则**无论如何都看不到**。
67
+ 探针:`@UseGuards(JwtAuthGuard)` 的 `createGroup` 被报
68
+ 「Authorization (Unauthenticated Mutation)」,而它明明有守卫。
69
+ - **更底层的发现**:规则侧定义了 23 个 `__progmune_*` 语义标记;Python 提取器产出
70
+ **23/23**,TS 提取器只产出 **4/23**(C/Go/Java **0/23**)。FP 观测池 100% 是 TS/JS
71
+ ⇒ 规则在 TS 上跑的时候 19 条证据通道是断的。本轮补的是其中一条(且是唯一
72
+ 「规则侧已经等着认、只是没人产」的那条)。
73
+ - **修复**(`src/extract-ir.ts`,类方法分支追加注入):
74
+ 类级 + 方法级装饰器 ⇒ `__progmune_auth_machinery__`(镜像 `tools/extract_ir.py:1134`
75
+ 的「类级框架守卫」)。**规则侧零改动**——Authorization 两条规则的 `auth_check`
76
+ safeguard 早已接受该标记。
77
+ - **边界(写死)**:`@Public()` / `@SkipAuth()` 显式免鉴权不注入;
78
+ `@UseGuards(ThrottlerGuard)` 等非鉴权守卫不注入;`@ApiBearerAuth()` 等 Swagger
79
+ 文档装饰器不注入;顶层函数装饰器不在本轮范围内。
80
+ - **实测效果**(FP 观测池 6 切片 / 367 条):Authorization 违规 **−24 条**
81
+ (Unauthenticated Access −11 / Mutation −13),**新增 0**,非 Authorization 规则
82
+ **0 变动**(与判据一致:只有接受标记的规则会受益;Input Validation 的 safeguard
83
+ 不接受任何标记,装饰器救不了它)。
84
+ - 回归:tsc ✓ / taintpath 160 条 ✓ / fr-007 pre 5·post 0、fr-016 pre 7·post 0 ✓ /
85
+ 定向测试 176 全绿(新增 E2 组 11 条)/ 反向验证四刀各落自己那组、残留 0。
86
+ **TS 盲测 LOST 0 / ADDED 0 —— 但这是空过**:generated 语料里装饰器命中数为 0,
87
+ 该门测不到本轮(R23 家族第三次),真证据来自 FP 观测池。
88
+
3
89
  ## [3.7.48] — 2026-09-22
4
90
 
5
91
  ### IR 提取失败必须发声(OOM 杀后的「0 违规假干净」根治)
@@ -56,6 +56,7 @@ const fs = __importStar(require("fs"));
56
56
  const os = __importStar(require("os"));
57
57
  const path = __importStar(require("path"));
58
58
  const extract_ir_1 = require("./extract-ir");
59
+ const protocol_detector_1 = require("./protocol-detector");
59
60
  const TSCONFIG = JSON.stringify({
60
61
  compilerOptions: {
61
62
  target: "ES2020",
@@ -2115,3 +2116,472 @@ export function dispatchTool(args: any) {
2115
2116
  }, 150000);
2116
2117
  }
2117
2118
  });
2119
+ // ═══════════════════════════════════════════════════════════════
2120
+ // E2:框架鉴权(装饰器)必须对规则可见(2026-09-22)
2121
+ //
2122
+ // 判据(先抄准再写 expect,R22):
2123
+ // protocol-detector.ts:398 / :430 —— Authorization (Unauthenticated Access /
2124
+ // Mutation) 两条规则的 auth_check safeguard 接受 __progmune_auth_machinery__;
2125
+ // 探针实测:@UseGuards(JwtAuthGuard) 的 createGroup 修复前报
2126
+ // 「Authorization (Unauthenticated Mutation)」,注入标记后该条消失,
2127
+ // 而 Input Validation 仍在(它的 safeguard 不接受任何标记 ⇒ 装饰器救不了它)。
2128
+ //
2129
+ // 反面风险(注入过头 = 假阴性,比误报更危险):
2130
+ // @Public()/@SkipAuth() 是「显式免鉴权」,不得注入;
2131
+ // @UseGuards(ThrottlerGuard) 是限流不是鉴权,不得注入;
2132
+ // @ApiBearerAuth() 只是 Swagger 文档,不实施鉴权,不得注入。
2133
+ // ═══════════════════════════════════════════════════════════════
2134
+ (0, vitest_1.describe)("E2:框架鉴权装饰器必须对规则可见(此前 TS 侧从未产出该标记)", () => {
2135
+ const AUTH_MARK = "__progmune_auth_machinery__";
2136
+ const cases = [
2137
+ {
2138
+ name: "正例·① 类级 @UseGuards(JwtAuthGuard)",
2139
+ fn: "GroupController.createGroup",
2140
+ files: {
2141
+ "a.ts": `
2142
+ declare const UseGuards: any;
2143
+ declare const Controller: any;
2144
+ declare const Post: any;
2145
+ declare class JwtAuthGuard {}
2146
+ @UseGuards(JwtAuthGuard)
2147
+ @Controller('groups')
2148
+ export class GroupController {
2149
+ createGroup(dto: any) { return this.svc.create(dto); }
2150
+ }
2151
+ `,
2152
+ },
2153
+ want: [AUTH_MARK],
2154
+ why: "NestJS 最常见的形态:鉴权写在类上,方法体内什么都没有 ⇒ 规则原来看不见",
2155
+ },
2156
+ {
2157
+ name: "正例·② 方法级 @UseGuards(JwtAuthGuard)",
2158
+ fn: "C.m",
2159
+ files: {
2160
+ "a.ts": `
2161
+ declare const UseGuards: any;
2162
+ declare class JwtAuthGuard {}
2163
+ export class C {
2164
+ @UseGuards(JwtAuthGuard)
2165
+ m() { return 1; }
2166
+ }
2167
+ `,
2168
+ },
2169
+ want: [AUTH_MARK],
2170
+ why: "守卫挂在单个端点上(而不是整个控制器)",
2171
+ },
2172
+ {
2173
+ name: "正例·③ @UseGuards(AuthGuard('jwt'))",
2174
+ fn: "C.m",
2175
+ files: {
2176
+ "a.ts": `
2177
+ declare const UseGuards: any;
2178
+ declare function AuthGuard(x: string): any;
2179
+ export class C {
2180
+ @UseGuards(AuthGuard('jwt'))
2181
+ m() { return 1; }
2182
+ }
2183
+ `,
2184
+ },
2185
+ want: [AUTH_MARK],
2186
+ why: "NestJS 官方 passport 形态:守卫是工厂调用,实参里含 jwt",
2187
+ },
2188
+ {
2189
+ name: "正例·④ @Roles('admin')",
2190
+ fn: "C.m",
2191
+ files: {
2192
+ "a.ts": `
2193
+ declare const Roles: any;
2194
+ export class C {
2195
+ @Roles('admin')
2196
+ m() { return 1; }
2197
+ }
2198
+ `,
2199
+ },
2200
+ want: [AUTH_MARK],
2201
+ why: "角色/权限装饰器同样是框架鉴权",
2202
+ },
2203
+ {
2204
+ name: "正例·⑤ @Authorize()(无实参)",
2205
+ fn: "C.m",
2206
+ files: {
2207
+ "a.ts": `
2208
+ declare const Authorize: any;
2209
+ export class C {
2210
+ @Authorize()
2211
+ m() { return 1; }
2212
+ }
2213
+ `,
2214
+ },
2215
+ want: [AUTH_MARK],
2216
+ why: "无实参装饰器:无法靠实参判断,按装饰器名判定(保守放行)",
2217
+ },
2218
+ {
2219
+ name: "负对照·① @Public() 显式免鉴权,不得注入",
2220
+ fn: "C.m",
2221
+ files: {
2222
+ "a.ts": `
2223
+ declare const UseGuards: any;
2224
+ declare const Public: any;
2225
+ declare class JwtAuthGuard {}
2226
+ @UseGuards(JwtAuthGuard)
2227
+ export class C {
2228
+ @Public()
2229
+ m() { return 1; }
2230
+ }
2231
+ `,
2232
+ },
2233
+ want: [],
2234
+ dontWant: [AUTH_MARK],
2235
+ why: "公开端点被判成「有鉴权」= 假阴性,比误报危险得多,必须挡住",
2236
+ },
2237
+ {
2238
+ name: "负对照·② @UseGuards(ThrottlerGuard) 是限流不是鉴权",
2239
+ fn: "C.m",
2240
+ files: {
2241
+ "a.ts": `
2242
+ declare const UseGuards: any;
2243
+ declare class ThrottlerGuard {}
2244
+ export class C {
2245
+ @UseGuards(ThrottlerGuard)
2246
+ m() { return 1; }
2247
+ }
2248
+ `,
2249
+ },
2250
+ want: [],
2251
+ dontWant: [AUTH_MARK],
2252
+ why: "同名装饰器、非鉴权语义 ⇒ 实参必须看起来像鉴权守卫(R19:分母里最容易漏的就是被拒掉的那些)",
2253
+ },
2254
+ {
2255
+ name: "负对照·③ @ApiBearerAuth() 只是 Swagger 文档",
2256
+ fn: "C.m",
2257
+ files: {
2258
+ "a.ts": `
2259
+ declare const ApiBearerAuth: any;
2260
+ export class C {
2261
+ @ApiBearerAuth()
2262
+ m() { return 1; }
2263
+ }
2264
+ `,
2265
+ },
2266
+ want: [],
2267
+ dontWant: [AUTH_MARK],
2268
+ why: "名字里带 auth,但不实施鉴权 ⇒ Api* 一律跳过",
2269
+ },
2270
+ {
2271
+ name: "负对照·④ 无装饰器不得注入",
2272
+ fn: "C.m",
2273
+ files: {
2274
+ "a.ts": `
2275
+ export class C { m() { return 1; } }
2276
+ `,
2277
+ },
2278
+ want: [],
2279
+ dontWant: [AUTH_MARK],
2280
+ why: "基线:没有鉴权证据就不许凭空给出鉴权",
2281
+ },
2282
+ {
2283
+ name: "负对照·⑤ 顶层函数(非类)不在本轮范围内",
2284
+ fn: "handler",
2285
+ files: {
2286
+ "a.ts": `
2287
+ declare const UseGuards: any;
2288
+ declare class JwtAuthGuard {}
2289
+ @UseGuards(JwtAuthGuard)
2290
+ export function handler() { return 1; }
2291
+ `,
2292
+ },
2293
+ want: [],
2294
+ dontWant: [AUTH_MARK],
2295
+ why: "本轮只覆盖类/方法(镜像 Python 的『类级守卫』)。顶层函数装饰器罕见,边界写死在此",
2296
+ },
2297
+ {
2298
+ name: "正对照·⑥ 真实调用与既有标记不得被挤掉(E1 不回归)",
2299
+ fn: "Uploader.read",
2300
+ files: {
2301
+ "a.ts": `
2302
+ import * as fs from "fs";
2303
+ declare const UseGuards: any;
2304
+ declare class JwtAuthGuard {}
2305
+ @UseGuards(JwtAuthGuard)
2306
+ export class Uploader {
2307
+ read(filePath: string) { return fs.readFileSync(filePath, "utf-8"); }
2308
+ }
2309
+ `,
2310
+ },
2311
+ want: [AUTH_MARK, "readFileSync"],
2312
+ why: "本轮是**追加**标记,不是替换;E1 补进来的真实调用必须仍在",
2313
+ },
2314
+ ];
2315
+ for (const c of cases) {
2316
+ (0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
2317
+ const dir = makeProject(c.files);
2318
+ try {
2319
+ const calls = marksFor(dir, c.fn);
2320
+ for (const w of c.want)
2321
+ (0, vitest_1.expect)(calls).toContain(w);
2322
+ for (const d of c.dontWant ?? [])
2323
+ (0, vitest_1.expect)(calls).not.toContain(d);
2324
+ }
2325
+ finally {
2326
+ fs.rmSync(dir, { recursive: true, force: true });
2327
+ }
2328
+ }, 150000);
2329
+ }
2330
+ });
2331
+ // ═══════════════════════════════════════════════════════════════
2332
+ // E3:DTO 的 schema 校验必须对规则可见(2026-09-22)
2333
+ //
2334
+ // 判据(先抄准再写 expect,R22):
2335
+ // protocol-detector.ts Input Validation 的 safeguard **此前不接受任何
2336
+ // __progmune_* 标记**(纯词形匹配)⇒ NestJS 把校验写在 DTO 属性装饰器上时,
2337
+ // 规则永远看不到。本轮是「接通道」:提取器产 __progmune_input_schema__ +
2338
+ // 规则侧在**抑制位**加一个接受分支。只做减法,不进 trigger。
2339
+ //
2340
+ // 反面风险(注入过头 = 假阴性):
2341
+ // DTO 只有 @IsOptional / @Type / @Transform(不校验)⇒ 不得注入;
2342
+ // 入参是基本类型 / 无入参 ⇒ 不得注入;
2343
+ // 标记不得顺带压掉别的规则(Data Integrity 曾因标记名含 validate 被误压,
2344
+ // 见 extract-ir.ts 里标记命名的说明 ⇒ 这是本组专门要守的)。
2345
+ // ═══════════════════════════════════════════════════════════════
2346
+ (0, vitest_1.describe)("E3:DTO 的 schema 校验必须对 Input Validation 可见", () => {
2347
+ const MARK = "__progmune_input_schema__";
2348
+ const DECLS = `
2349
+ declare function Body(): any;
2350
+ declare function IsString(): any;
2351
+ declare function IsUUID(a?: any, b?: any): any;
2352
+ declare function MinLength(n: number): any;
2353
+ declare function IsOptional(): any;
2354
+ declare function Type(fn: any): any;
2355
+ declare function Transform(fn: any): any;
2356
+ `;
2357
+ const cases = [
2358
+ {
2359
+ name: "正例·① 跨文件 DTO(属性装饰器 @IsString)",
2360
+ fn: "GroupController.createGroup",
2361
+ files: {
2362
+ "dto.ts": DECLS + `
2363
+ export class CreateGroupDto {
2364
+ @IsString()
2365
+ name: string;
2366
+ }
2367
+ `,
2368
+ "a.ts": `
2369
+ import { CreateGroupDto } from "./dto";
2370
+ export class GroupController {
2371
+ createGroup(@Body() dto: CreateGroupDto) { return this.svc.createGroup(dto); }
2372
+ }
2373
+ `,
2374
+ },
2375
+ want: [MARK],
2376
+ why: "NestJS 最常见形态:校验写在另一个文件的 DTO 类上,函数体内什么都没有",
2377
+ },
2378
+ {
2379
+ name: "正例·② 同文件 DTO",
2380
+ fn: "C.createGroup",
2381
+ files: {
2382
+ "a.ts": DECLS + `
2383
+ export class CreateGroupDto {
2384
+ @IsUUID()
2385
+ id: string;
2386
+ }
2387
+ export class C {
2388
+ createGroup(dto: CreateGroupDto) { return 1; }
2389
+ }
2390
+ `,
2391
+ },
2392
+ want: [MARK],
2393
+ why: "DTO 与消费者同文件(小模块常见)",
2394
+ },
2395
+ {
2396
+ name: "正例·③ @MinLength 也算校验(不只是 Is* 族)",
2397
+ fn: "C.createGroup",
2398
+ files: {
2399
+ "a.ts": DECLS + `
2400
+ export class D {
2401
+ @MinLength(2)
2402
+ name: string;
2403
+ }
2404
+ export class C {
2405
+ createGroup(dto: D) { return 1; }
2406
+ }
2407
+ `,
2408
+ },
2409
+ want: [MARK],
2410
+ why: "R19:分母不只是 Is* 族;MinLength/MaxLength/Matches 都是真校验",
2411
+ },
2412
+ {
2413
+ name: "正例·④ 顶层函数同样覆盖(与 E2 的『仅类/方法』边界不同)",
2414
+ fn: "createGroup",
2415
+ files: {
2416
+ "a.ts": DECLS + `
2417
+ export class D {
2418
+ @IsString()
2419
+ name: string;
2420
+ }
2421
+ export function createGroup(dto: D) { return 1; }
2422
+ `,
2423
+ },
2424
+ want: [MARK],
2425
+ why: "按入参判定,不按函数形态 ⇒ 顶层函数同样接得上(E2 那只覆盖装饰器,本轮不看装饰器)",
2426
+ },
2427
+ {
2428
+ name: "负对照·① DTO 只有 @IsOptional/@Type/@Transform —— 不是校验",
2429
+ fn: "C.createGroup",
2430
+ files: {
2431
+ "a.ts": DECLS + `
2432
+ export class D {
2433
+ @IsOptional()
2434
+ @Type(() => String)
2435
+ @Transform(({ value }) => value)
2436
+ name?: string;
2437
+ }
2438
+ export class C {
2439
+ createGroup(dto: D) { return 1; }
2440
+ }
2441
+ `,
2442
+ },
2443
+ want: [],
2444
+ dontWant: [MARK],
2445
+ why: "可缺省 + 类型转换 ≠ 校验;判成已校验就是假阴性",
2446
+ },
2447
+ {
2448
+ name: "负对照·② 入参是基本类型(不是 DTO 类)",
2449
+ fn: "C.createGroup",
2450
+ files: {
2451
+ "a.ts": `
2452
+ export class C { createGroup(name: string) { return 1; } }
2453
+ `,
2454
+ },
2455
+ want: [],
2456
+ dontWant: [MARK],
2457
+ why: "基线:没有 schema 就不许凭空给出校验证据",
2458
+ },
2459
+ {
2460
+ name: "负对照·③ 无入参",
2461
+ fn: "C.createGroup",
2462
+ files: {
2463
+ "a.ts": DECLS + `
2464
+ export class D { @IsString() name: string; }
2465
+ export class C { createGroup() { return 1; } }
2466
+ `,
2467
+ },
2468
+ want: [],
2469
+ dontWant: [MARK],
2470
+ why: "DTO 存在但没被当入参 ⇒ 该函数的输入没有被校验",
2471
+ },
2472
+ {
2473
+ name: "正对照·⑤ 真实调用不得被挤掉(E1 不回归)",
2474
+ fn: "C.createGroup",
2475
+ files: {
2476
+ "a.ts": DECLS + `
2477
+ export class D { @IsString() name: string; }
2478
+ export class C {
2479
+ createGroup(dto: D) { return this.repo.createGroup(dto); }
2480
+ }
2481
+ `,
2482
+ },
2483
+ want: [MARK, "createGroup"],
2484
+ why: "本轮是**追加**标记;E1 补进来的真实调用必须仍在",
2485
+ },
2486
+ ];
2487
+ for (const c of cases) {
2488
+ (0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
2489
+ const dir = makeProject(c.files);
2490
+ try {
2491
+ const calls = marksFor(dir, c.fn);
2492
+ for (const w of c.want)
2493
+ (0, vitest_1.expect)(calls).toContain(w);
2494
+ for (const d of c.dontWant ?? [])
2495
+ (0, vitest_1.expect)(calls).not.toContain(d);
2496
+ }
2497
+ finally {
2498
+ fs.rmSync(dir, { recursive: true, force: true });
2499
+ }
2500
+ }, 150000);
2501
+ }
2502
+ // ── 规则侧:标记必须真的压得住 Input Validation,且只压这一条 ──
2503
+ (0, vitest_1.it)("规则侧·① 注入标记后 Input Validation 必须消失", () => {
2504
+ const before = (0, protocol_detector_1.detectSafeguardViolations)(["createPost"], "createPost");
2505
+ (0, vitest_1.expect)(before.some(v => v.rule === "Input Validation")).toBe(true);
2506
+ const after = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", MARK], "createPost");
2507
+ (0, vitest_1.expect)(after.some(v => v.rule === "Input Validation")).toBe(false);
2508
+ });
2509
+ (0, vitest_1.it)("规则侧·② 标记不得压掉别的规则(Authorization 仍在)", () => {
2510
+ const after = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", MARK], "createPost");
2511
+ (0, vitest_1.expect)(after.some(v => v.category === "authorization")).toBe(true);
2512
+ });
2513
+ (0, vitest_1.it)("规则侧·③ 标记名不得撞上 Data Integrity 的 safeguard(命名事故回归)", () => {
2514
+ // 曾用的名字 __progmune_validated_input__ 会被
2515
+ // \b(get|find|check|exists|lookup|status|validate|verify)(?:[A-Z]\w*|_\w+)\b 命中,
2516
+ // 凭空压掉 Foreign Key 校验。改名后必须仍报。
2517
+ const withOld = (0, protocol_detector_1.detectSafeguardViolations)(["addChild", "__progmune_validated_input__"], "addChild", undefined, ["parentId"], false);
2518
+ const withNew = (0, protocol_detector_1.detectSafeguardViolations)(["addChild", MARK], "addChild", undefined, ["parentId"], false);
2519
+ (0, vitest_1.expect)(withOld.some(v => v.rule === "Data Integrity (Foreign Key)")).toBe(false);
2520
+ (0, vitest_1.expect)(withNew.some(v => v.rule === "Data Integrity (Foreign Key)")).toBe(true);
2521
+ });
2522
+ });
2523
+ (0, vitest_1.describe)("F:Input Validation 的 trigger 只认函数名(不再被 callee 拖下水)", () => {
2524
+ // 立项依据(2026-09-22):本规则的陈述对象是「这个函数自己是不是内容创建函数」
2525
+ // (violationMessage 原文:"Content creation function does not validate...")。
2526
+ // 此前拿 callee 名字去匹配 trigger,产生两类缺陷,各占一半:
2527
+ // (a) 重复投射 —— 分发器 handleRequest 因调用 createEvent/createRoute 被计一次,
2528
+ // 而那些 callee 自己也在报(实测 100/175,全部 SAFE_DUP)。
2529
+ // (b) 理由错 —— login/register/authenticate 因调用 **createHash**(密码哈希)
2530
+ // 被判成「内容创建函数未校验输入」(69/175)。
2531
+ // 175 条已逐条归类,无一为真阳性;另有 6 条 list/get 查询函数。
2532
+ const IV = (calls, own) => (0, protocol_detector_1.detectSafeguardViolations)(calls, own).filter(v => v.rule === "Input Validation");
2533
+ const hit = (calls, own) => IV(calls, own).length > 0;
2534
+ // ── 正例:函数名自己就是创建函数 ⇒ 必须仍然报(本轮不得削弱真阳性) ──
2535
+ (0, vitest_1.it)("正例·① createPost 自身仍报", () => {
2536
+ (0, vitest_1.expect)(hit(["db.save"], "createPost")).toBe(true);
2537
+ });
2538
+ (0, vitest_1.it)("正例·② 类方法 C.createPost 仍报(ownName 取末段)", () => {
2539
+ (0, vitest_1.expect)(hit(["db.save"], "C.createPost")).toBe(true);
2540
+ });
2541
+ (0, vitest_1.it)("正例·③ addComment 仍报", () => {
2542
+ (0, vitest_1.expect)(hit(["db.save"], "addComment")).toBe(true);
2543
+ });
2544
+ (0, vitest_1.it)("正例·④ uploadFile 仍报", () => {
2545
+ (0, vitest_1.expect)(hit(["fs.write"], "uploadFile")).toBe(true);
2546
+ });
2547
+ (0, vitest_1.it)("正例·⑤ 下划线形态 create_post 仍报", () => {
2548
+ (0, vitest_1.expect)(hit(["db.save"], "create_post")).toBe(true);
2549
+ });
2550
+ // ── 负例:靠 callee 触发 ⇒ 必须不再报 ──
2551
+ (0, vitest_1.it)("负例·① 分发器 handleRequest 不再因 createEvent 被计(重复投射)", () => {
2552
+ (0, vitest_1.expect)(hit(["createEvent", "createMetric"], "handleRequest")).toBe(false);
2553
+ });
2554
+ (0, vitest_1.it)("负例·② registerNewUser 不再因 createHash 被判内容创建(密码哈希)", () => {
2555
+ (0, vitest_1.expect)(hit(["createHash", "bcrypt.hash"], "registerNewUser")).toBe(false);
2556
+ });
2557
+ (0, vitest_1.it)("负例·③ doLogin 同上", () => {
2558
+ (0, vitest_1.expect)(hit(["createHash"], "doLogin")).toBe(false);
2559
+ });
2560
+ (0, vitest_1.it)("负例·④ authenticate 同上", () => {
2561
+ (0, vitest_1.expect)(hit(["createHash"], "authenticate")).toBe(false);
2562
+ });
2563
+ (0, vitest_1.it)("负例·⑤ 查询函数 listPosts 不再被报", () => {
2564
+ (0, vitest_1.expect)(hit(["sort", "filter"], "listPosts")).toBe(false);
2565
+ });
2566
+ (0, vitest_1.it)("负例·⑥ ORM/建表类 callee 不再拖累调用者(createQueryBuilder)", () => {
2567
+ (0, vitest_1.expect)(hit(["createQueryBuilder"], "findArticles")).toBe(false);
2568
+ });
2569
+ // ── 边界与回归保护 ──
2570
+ (0, vitest_1.it)("边界·① ownName 缺失时不得误触发(triggerCalls 为空数组)", () => {
2571
+ (0, vitest_1.expect)(hit(["createPost"], "")).toBe(false);
2572
+ });
2573
+ (0, vitest_1.it)("回归·① E3 的 schema 标记仍然压得住(本轮不得破坏 E3)", () => {
2574
+ (0, vitest_1.expect)(hit(["createPost", "__progmune_input_schema__"], "createPost")).toBe(false);
2575
+ (0, vitest_1.expect)(hit(["createPost"], "createPost")).toBe(true);
2576
+ });
2577
+ (0, vitest_1.it)("回归·② 非目标规则不受影响(Authorization 仍在)", () => {
2578
+ const v = (0, protocol_detector_1.detectSafeguardViolations)(["createEvent"], "handleRequest");
2579
+ (0, vitest_1.expect)(v.some(x => x.category === "authorization")).toBe(true);
2580
+ });
2581
+ // ── 已知边界(登记,不断言)──
2582
+ // 函数名不含 create/add/post/upload、但确实是「入口把用户输入交给内容创建」
2583
+ // 的函数(如 handleSubmit 调 commentRepo.create,而 create 不在扫描范围内),
2584
+ // 本轮之后不再报。这是 triggerOwnNameOnly 的固有代价:175 条损失里此类为 0,
2585
+ // real-world 池里亦未发现(27 条触发源全为 ORM/哈希/权限工厂/内部工具)。
2586
+ // 若要补回,需要 `exposed`(web handler)通道 —— 按 R27 先查该通道通不通。
2587
+ });
@@ -390,6 +390,125 @@ function extractDirectCalls(func, preText) {
390
390
  // 与 protocol-detector 的同名规则(source-level benchmark 路径)。
391
391
  // ═══════════════════════════════════════════════════════════════
392
392
  const PATH_TRAVERSAL_MARKER = "__progmune_path_traversal__";
393
+ // ═══════════════════════════════════════════════════════════════
394
+ // E2(2026-09-22):TS 侧「框架鉴权」语义标记 —— 镜像 tools/extract_ir.py:1134
395
+ //
396
+ // Python 侧由**类级框架守卫**触发(类名含 authenticator / DRF permission_classes);
397
+ // TS 侧的等价物是 NestJS 的类/方法级守卫装饰器:@UseGuards(JwtAuthGuard)、@Roles(...) 等。
398
+ // 装饰器里的鉴权既不在函数体文本里、也不在 calls 里,规则**无论如何都看不到**——
399
+ // 探针:@UseGuards(JwtAuthGuard) 的 createGroup 被报「未鉴权」,注入本标记后该条消失。
400
+ //
401
+ // 消费方:protocol-detector 的 Authorization (Unauthenticated Access / Mutation)
402
+ // 两条规则的 auth_check safeguard(protocol-detector.ts:398 / :430)——它们**早已接受**
403
+ // __progmune_auth_machinery__,TS 侧此前从未产出 ⇒ 规则侧零改动。
404
+ //
405
+ // 边界(写死在这里,别想当然):只有**接受标记的规则**会受益。
406
+ // Input Validation 的 safeguard 不接受任何 __progmune_* 标记(protocol-detector.ts:481
407
+ // 是纯词形匹配),装饰器对它无效;实测注入后 Input Validation 条仍在。
408
+ // ═══════════════════════════════════════════════════════════════
409
+ const AUTH_MACHINERY_MARKER = "__progmune_auth_machinery__";
410
+ /** 显式免鉴权装饰器 —— 出现即不注入。把公开端点判成「有鉴权」是最危险的假阴性。 */
411
+ const AUTH_SKIP_DECORATOR = /^(?:Public|SkipAuth|AllowAnonymous|IsPublic|NoAuth|ApiExcludeEndpoint)$/i;
412
+ /** 承载鉴权的装饰器名(NestJS 约定:Guard = 鉴权/授权)。 */
413
+ const AUTH_DECORATOR = /^(?:UseGuards|Roles|Permissions|Authorize|RequireAuth|RequiresAuth|Authenticated|UseAuth|CheckPermissions|RequirePermissions|AdminOnly)$/i;
414
+ /** @UseGuards(X) 的实参须像鉴权守卫 —— 用于排除 @UseGuards(ThrottlerGuard) 之类。 */
415
+ const GUARD_ARG_AUTH = /auth|jwt|session|login|permission|role|admin|bearer|credential/i;
416
+ /**
417
+ * 由类/方法装饰器判定是否注入框架鉴权标记。文本匹配(与 computeMarkerCalls 口径一致)。
418
+ * @returns [] 或 [AUTH_MACHINERY_MARKER]
419
+ */
420
+ function authMachineryFromDecorators(decorators) {
421
+ let skip = false;
422
+ let auth = false;
423
+ for (const d of decorators) {
424
+ const text = (d?.getText?.() ?? "").trim();
425
+ const name = /^@([A-Za-z_$][\w$]*)/.exec(text)?.[1];
426
+ if (!name)
427
+ continue;
428
+ if (AUTH_SKIP_DECORATOR.test(name)) {
429
+ skip = true;
430
+ continue;
431
+ }
432
+ // Swagger 文档装饰器(@ApiBearerAuth …)只描述、不实施鉴权
433
+ if (/^Api/i.test(name))
434
+ continue;
435
+ if (!AUTH_DECORATOR.test(name) && !/guard|auth/i.test(name))
436
+ continue;
437
+ const args = /^@[\w$]*\(([\s\S]*)\)$/.exec(text)?.[1] ?? "";
438
+ if (args.trim() && !GUARD_ARG_AUTH.test(args))
439
+ continue;
440
+ auth = true;
441
+ }
442
+ return auth && !skip ? [AUTH_MACHINERY_MARKER] : [];
443
+ }
444
+ // ═══════════════════════════════════════════════════════════════
445
+ // E3(2026-09-22):TS 侧「入参已按 schema 校验」语义标记
446
+ //
447
+ // 判据(先抄准再写 expect,R22):
448
+ // Input Validation 的 safeguard(protocol-detector.ts:481)此前是**纯词形匹配**,
449
+ // 不接受任何 __progmune_* 标记 ⇒ NestJS 把校验写在 DTO 类的属性装饰器上时,
450
+ // 规则无论如何都看不到 —— 这是 R27 说的「证据通道断着」。
451
+ // 本轮做的是「接通道」:提取器产标记 + 规则侧开一个接受位,**只抑制,不新增触发**。
452
+ //
453
+ // 为什么标记名叫 __progmune_input_schema__ 而不是 ..._validated_input__:
454
+ // 标记本身会被别的规则当普通调用名匹配。实测 __progmune_validated_input__ 会
455
+ // 撞上 Data Integrity (Foreign Key) 的 safeguard
456
+ // `\b(get|find|check|exists|lookup|status|validate|verify)(?:[A-Z]\w*|_\w+)\b`
457
+ // ("validate" + "d_input__" 命中),凭空压掉 10 条无关违规。
458
+ // 命名必须避开 get/find/check/verify/validate/create/add/post/upload 等动词词根。
459
+ //
460
+ // 边界(写死在这里):只在**入参类型**是本项目里带校验装饰器的类时才注入;
461
+ // 装饰器的「校验性」判定见下,@IsOptional / @Type / @Transform 单独出现不算。
462
+ // ═══════════════════════════════════════════════════════════════
463
+ const INPUT_SCHEMA_MARKER = "__progmune_input_schema__";
464
+ /** 前缀式:MinLength / MaxLength / Matches / ValidateNested … 都是真校验 */
465
+ const VALIDATOR_DECORATOR_PREFIX = /^(?:Min|Max|Length|Matches|ArrayMinSize|ArrayMaxSize|Validate|Equals|NotEquals|Allow|Contains|NotContains)/;
466
+ /** class-validator 的 Is* 族;@IsOptional 只是「可缺省」,不校验 ⇒ 排除 */
467
+ const VALIDATOR_DECORATOR_IS = /^Is(?!Optional$)[A-Z]/;
468
+ function decoratorIsValidator(text) {
469
+ const name = /^@([A-Za-z_$][\w$]*)/.exec((text ?? "").trim())?.[1];
470
+ if (!name)
471
+ return false;
472
+ return VALIDATOR_DECORATOR_PREFIX.test(name) || VALIDATOR_DECORATOR_IS.test(name);
473
+ }
474
+ /**
475
+ * 全项目扫描:类自身或其属性上带校验装饰器的类名集合。
476
+ * 只看类名(不做类型解析)—— 与污点管线口径一致,跨文件引用靠名字对上。
477
+ */
478
+ function validatedDtoClassNames(project) {
479
+ const out = new Set();
480
+ for (const sf of project.getSourceFiles()) {
481
+ if (sf.getFilePath().includes("node_modules"))
482
+ continue;
483
+ for (const cls of sf.getClasses()) {
484
+ const name = cls.getName();
485
+ if (!name)
486
+ continue;
487
+ let hit = false;
488
+ for (const d of cls.getDecorators()) {
489
+ if (decoratorIsValidator(d?.getText?.() ?? "")) {
490
+ hit = true;
491
+ break;
492
+ }
493
+ }
494
+ if (!hit) {
495
+ for (const prop of cls.getProperties()) {
496
+ for (const d of prop.getDecorators()) {
497
+ if (decoratorIsValidator(d?.getText?.() ?? "")) {
498
+ hit = true;
499
+ break;
500
+ }
501
+ }
502
+ if (hit)
503
+ break;
504
+ }
505
+ }
506
+ if (hit)
507
+ out.add(name);
508
+ }
509
+ }
510
+ return out;
511
+ }
393
512
  /**
394
513
  * 标识符是否「看起来像基目录」—— G-A2 的定案依据。
395
514
  *
@@ -2877,6 +2996,15 @@ function _extractSingleProject(absRoot, tsconfigPath) {
2877
2996
  // 顺序与函数声明 / 箭头两个分支保持一致:先真实调用,再追加语义标记。
2878
2997
  const mCalls = extractDirectCalls(m, mText);
2879
2998
  mCalls.push(...computeMarkerCalls(mText, mParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns, directGuardFns, shapers));
2999
+ // E2(2026-09-22):类级 + 方法级守卫装饰器 → 框架鉴权标记。鉴权写在装饰器里,
3000
+ // 函数体文本与 calls 都看不到(镜像 Python 提取器的「类级框架守卫」处理)。
3001
+ for (const mk of authMachineryFromDecorators([
3002
+ ...(cls.getDecorators?.() ?? []),
3003
+ ...(m.getDecorators?.() ?? []),
3004
+ ])) {
3005
+ if (!mCalls.includes(mk))
3006
+ mCalls.push(mk);
3007
+ }
2880
3008
  funcs.push({
2881
3009
  name: `${cn}.${mn}`,
2882
3010
  params: mParams.map((p) => ({ name: p.getName(), type: getParamType(p), typeDetail: getParamTypeDetail(p) })),
@@ -2909,6 +3037,20 @@ function _extractSingleProject(absRoot, tsconfigPath) {
2909
3037
  }
2910
3038
  }
2911
3039
  }
3040
+ const validatedDtoClasses = validatedDtoClassNames(project);
3041
+ // ── E3(2026-09-22):入参已按 schema 校验 ⇒ 注入 __progmune_input_schema__
3042
+ // 放在主循环之后统一应用,覆盖全部函数形态(函数声明 / 箭头 / 类方法),
3043
+ // 与 pendingMethodMarks 的跨函数标记同一处收口。
3044
+ for (const f of funcs) {
3045
+ if (!f.params || f.params.length === 0)
3046
+ continue;
3047
+ const hit = f.params.some((p) => p.type && validatedDtoClasses.has(p.type));
3048
+ if (!hit)
3049
+ continue;
3050
+ f.calls = f.calls || [];
3051
+ if (!f.calls.includes(INPUT_SCHEMA_MARKER))
3052
+ f.calls.push(INPUT_SCHEMA_MARKER);
3053
+ }
2912
3054
  // ═══════════════════════════════════════════════════════════════
2913
3055
  // Phase 5: Dynamic external function resolution
2914
3056
  // Replaces hardcoded knownExternals
@@ -429,9 +429,20 @@ const SAFEGUARD_RULES = [
429
429
  {
430
430
  name: "Input Validation",
431
431
  category: "input_validation",
432
+ // F 轮(2026-09-22):本规则的陈述对象是「这个函数自己是不是内容创建函数」
433
+ // (violationMessage: "Content creation function ...")。此前拿 callee 名字
434
+ // 去匹配 trigger,造成两类缺陷:① 分发器 handleRequest 因调用 createEvent 等
435
+ // 被重复计一次,而那些 callee 自己也在报;② login/register 因调用
436
+ // **createHash**(密码哈希)被判成「内容创建函数未校验输入」。
437
+ // 175 条损失已逐条归类,无一为真阳性。⇒ trigger 只看函数名。
438
+ triggerOwnNameOnly: true,
432
439
  trigger: /\b(create|add|post|upload)(?:[A-Z]\w*|_\w+)|(?:[A-Z]\w*|_\w+)(Create|Add|Post|Upload)\b/i,
433
440
  safeguards: [
434
- { pattern: /\b(validate|sanitize|check|verify)(?:[A-Z]\w*|_\w*)(Content|Input|Length|Title|Body|Type|Size|File|Data|Param|Arg|Field|Value)\b|\b(validateContent|sanitizeInput|checkLength|verifyType|checkSize)\b/i, label: "input_validation" },
441
+ // E3(2026-09-22):__progmune_input_schema__ —— 提取器在「入参类型是带校验
442
+ // 装饰器的 DTO 类」时产出(NestJS 把校验写在 DTO 属性装饰器上,词形匹配永远
443
+ // 看不到)。本规则此前不接受任何 __progmune_* 标记 ⇒ 该证据通道是断的。
444
+ // 注意只加在**抑制位**(safeguard),不进 trigger:新通道只能做减法。
445
+ { pattern: /\b(validate|sanitize|check|verify)(?:[A-Z]\w*|_\w*)(Content|Input|Length|Title|Body|Type|Size|File|Data|Param|Arg|Field|Value)\b|\b(validateContent|sanitizeInput|checkLength|verifyType|checkSize)\b|__progmune_input_schema__/i, label: "input_validation" },
435
446
  ],
436
447
  violationMessage: "Content creation function does not validate or sanitize input. XSS, injection, and oversized content possible.",
437
448
  conceptMissing: ["InputSanitization", "ContentValidation", "SizeLimit"],
@@ -1129,7 +1140,11 @@ function detectSafeguardViolations(calls, enclosingFuncName, language, params, e
1129
1140
  : SAFEGUARD_RULES;
1130
1141
  for (const rule of activeRules) {
1131
1142
  // Check if trigger matches
1132
- const triggerCalls = rule.triggerCallsOnly ? rawCalls : effectiveCalls;
1143
+ const triggerCalls = rule.triggerOwnNameOnly
1144
+ ? (ownName ? [ownName] : [])
1145
+ : rule.triggerCallsOnly
1146
+ ? rawCalls
1147
+ : effectiveCalls;
1133
1148
  const triggerMatch = triggerCalls.some(c => rule.trigger.test(c));
1134
1149
  if (!triggerMatch)
1135
1150
  continue;
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.48";
23
+ exports.RUNTIME_VERSION = "3.7.51";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.48",
3
+ "version": "3.7.51",
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/",