progmune-runtime 3.7.49 → 3.7.52

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,82 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.52] — 2026-09-23
4
+
5
+ ### 语料补形 + 两处基础设施修复 + E4/E5 两个确定性通道(§26-§31 合并发布)
6
+
7
+ **语料补形(§26,R23 家族的处置而非第五条注记)**:`webshape_A..F` 六族钉进 generated/——NestJS 装饰器/DTO 校验/类方法入口/分发器/上传校验证据/副作用 sink 形状;`check-webshape.ts` 闸门(默认实时提取、语料缺失即硬失败);期望表由生成器落盘(R7);补形后 exposed 通道两变体的离线重放得以全量验证——增量召回 2 条真报 vs 70 条已知误报 ⇒ **exposed 通道按 R33 判不做**(做完也不划算,理由写死进设计稿 §25)。
8
+
9
+ **基础设施两修(§27)**:
10
+ - 死切片复活:solution 风格仓库的根 tsconfig 是容器 ⇒ 提取器 0 源文件静默通过(gothinkster 37 文件 0 函数污染所有分母)——修复=零源文件兜底回退 `**/*.ts` + stderr 告警(115 generated 项目 0 误触发)+ 抓取器保留 tsconfig 变体
11
+ - language 门:TS 扫描一直传 undefined ⇒ activeRules 退化「全部规则」,Python/C 专属规则套在 TS 上(池少 5 条 Context Manager Usage)——batch-scan/fp-pool-scan 补 "typescript"
12
+ - R36:产出 0 的环节必须自证扫到了东西;R37 判据冲突检测(当场逼出 L7 虚高 3 条真报);R38 口径可比性;R39 gold 分层+多数类基线;R40 外部服务先无效凭据验证
13
+
14
+ **E4(§30):判据瞎,不是判据不准**(R41 缺信息 vs 缺语义)——`inputGuardFunctionKeys` 预扫函数体校验证据(limits:{…}+HTTP 异常抛掷)⇒ `__progmune_input_guard__`(只进抑制位);webshape_E 5 条正反钉死;池 −5 条全部逐条确证(3 条在探针预测外——探针漏箭头常量形态)
15
+
16
+ **E5(§31):参数流向外部副作用的语义前置**(先量化再实现,R43)——`inputEffectFunctionKeys` + `requireMarkerLanguages`(R42:不限定语言 Python 侧该规则整体归零而四门看不见);真值集预演:TP 召回 100%(17/17)、FP 压制 77.2%,粗判据取舍(丢真报比留误报贵);池 409→364、IV 75→30、**verified TP 6/6 零丢失、verified FP 29/29 全压**;盲测 −216 全 IV=口径修正(generated 语料无真实 sink),webshape_F 补真实 sink 形状 3 条照常报
17
+
18
+ **方法学累计**:R34(反向验证期望须由期望表推导,本轮第三次同形踩坑——推导逻辑应工具化)、R41-R43;fp-gold.jsonl 真值集入库(verified 53 条/12.8%——启发式不能当 gold 的实证);jev 实验台(无 key,401 确认端点,ECE/多数类基线硬指标,绝不进扫描路径)
19
+
20
+ **门**:build ✓ / taintpath 160/160 / webshape 29/29 / fr-007 pre5·post0 + fr-016 pre7·post0 / 定向 212 全绿 / 盲测 LOST 391 IV+1 已知 G2 项、ADDED 全在新语料 / 池 8 片 573 文件 IV=30 ✓
21
+
22
+ ## [3.7.51] — 2026-09-22
23
+
24
+ ### Input Validation 的 trigger 只认函数名:不再被 callee 拖下水(F 轮)
25
+
26
+ - **缺口(不是词表问题,是「陈述对象」错位)**:本规则的 violationMessage 是
27
+ *"Content creation function does not validate…"* —— 陈述对象是
28
+ **「这个函数自己是不是内容创建函数」**。但 trigger 此前拿 **callee 名字**
29
+ 去匹配 `create*|add*|post*|upload*`,于是产生两类缺陷:
30
+ 1. **重复投射**:分发器 `handleRequest` 因调用 `createEvent` / `createRoute`
31
+ 被计一次,而那些 callee 自己也在报 ⇒ 同一条问题计两遍;
32
+ 2. **理由错**:`login` / `registerNewUser` / `authenticate` 因调用
33
+ **`createHash`**(密码哈希)被判成「内容创建函数未校验输入」。
34
+ - **修复**:新增规则字段 `triggerOwnNameOnly`(只拿 enclosing function 自身名字
35
+ 去匹配 trigger),在 `detectSafeguardViolations` 里加分支,并在
36
+ Input Validation 规则上开启。其余规则行为不变。
37
+ - **175 条损失逐条归类,无一真阳性**(R30):100 条重复投射(callee 自己仍报)、
38
+ 69 条 `createHash`、6 条 `list/get` 查询函数。真实池 37 条同理
39
+ (7 重复投射 / 3 被其他规则覆盖 / 27 触发源为 ORM·哈希·权限工厂·内部工具)。
40
+ - **实测**:TS 盲测 Input Validation **417 → 242(−175)**、ADDED 0、
41
+ **其他规则 0 变动**;FP 池 docmost 24 → 14(−10)、ADDED 0。
42
+ 离线重放(R29)的预测与真实重扫**逐条一致**(第二次确认保真度)。
43
+ - **已知边界(不掩饰)**:函数名不含 create/add/post/upload、但确实是
44
+ 「入口把用户输入交给内容创建」的函数(如 `handleSubmit` 调
45
+ `commentRepo.create`,而 create 不在扫描范围内)本轮后不再报。175 条里此类
46
+ 为 0;若要补回需要 `exposed`(web handler)通道 —— **按 R27 先查通道**。
47
+
48
+ ## [3.7.50] — 2026-09-22
49
+
50
+ ### TS 侧「入参已按 schema 校验」语义标记:DTO 的校验第一次对规则可见(E3)
51
+
52
+ - **缺口(R27 说的「证据通道断着」的实例)**:Input Validation 的 safeguard 是
53
+ **纯词形匹配,不接受任何 `__progmune_*` 标记**(`protocol-detector.ts:481`)。
54
+ NestJS 把校验写在**另一个文件**的 DTO 类属性装饰器上(`@IsString()` / `@MinLength(2)`),
55
+ 既不在函数体文本里、也不在 `calls` 里 ⇒ 规则永远看不到。E2 结束时已实测确认
56
+ 「装饰器救不了 Input Validation」,本轮是**接通道**,不是调词表。
57
+ - **修复(双侧,各一行)**:
58
+ - 提取器 `src/extract-ir.ts`:新增 `validatedDtoClassNames(project)` 预扫
59
+ (类自身或其属性上带校验装饰器 ⇒ 类名入集合),主循环后统一应用 ——
60
+ 入参类型是集合中的类 ⇒ 注入 `__progmune_input_schema__`。
61
+ 按**入参**判定、不按函数形态 ⇒ 函数声明 / 箭头 / 类方法全形态覆盖。
62
+ - 规则 `protocol-detector.ts`:Input Validation 的 safeguard 追加一个接受分支。
63
+ **只加在抑制位,不进 trigger**——新通道只能做减法。
64
+ - **标记命名是一个真陷阱(本轮抓到的)**:初版取名 `__progmune_validated_input__`,
65
+ 离线重放发现它会被 `Data Integrity (Foreign Key)` 的 safeguard
66
+ `\b(…|validate|…)(?:[A-Z]\w*|_\w+)\b` 命中(`validate` + `d_input__`),
67
+ 凭空压掉 **10 条无关违规**。改名 `__progmune_input_schema__`(不含
68
+ get/find/check/verify/validate/create/add/post/upload 任何动词词根)后归零。
69
+ 已加定向用例守住这条。
70
+ - **实测效果**(FP 观测池,docmost 切片真实重扫):Input Validation **39 → 24(−15)**,
71
+ **新增 0**,其他规则 **0 变动**。全池口径 −17(含 lujakob −1、w3tecch −1)。
72
+ - 回归:tsc ✓ / taintpath 160 条 ✓ / fr-007 pre 5·post 0、fr-016 pre 7·post 0 ✓ /
73
+ 定向测试 **187 全绿**(新增 E3 组 11 条)/ 反向验证两刀(拆规则侧接受位、关提取器
74
+ 发射)各自精确回到 39、残留 0。
75
+ - **TS 盲测 3201 → 3201、逐函数差异 0 —— 这是空过(R23 家族第四次)**,已核实:
76
+ generated 语料里校验装饰器命中数 **0**,该门测不到本轮;真证据来自 FP 观测池。
77
+ - 另附:本轮先用**离线重放**(拿已记录的 `calls + name` 直接喂 detector,不重新提取 IR)
78
+ 预测量三个候选变体的效果,再挑一个实现。重放预测的 −15 与真实重扫**逐条一致**。
79
+
3
80
  ## [3.7.49] — 2026-09-22
4
81
 
5
82
  ### TS 侧「框架鉴权」语义标记:装饰器里的鉴权第一次对规则可见(E2)
@@ -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",
@@ -2327,3 +2328,334 @@ export class Uploader {
2327
2328
  }, 150000);
2328
2329
  }
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
+ /** E5(2026-09-23):提取器在「参数流向外部副作用」时注入的语义前置标记。
2347
+ * Input Validation 规则据此 requireMarker —— 名字叫 create/add 但根本不接外部
2348
+ * 输入的函数(配置装配、工厂)不该被要求「必须校验输入」。 */
2349
+ const INPUT_EFFECT = "__progmune_input_effect__";
2350
+ (0, vitest_1.describe)("E3:DTO 的 schema 校验必须对 Input Validation 可见", () => {
2351
+ const MARK = "__progmune_input_schema__";
2352
+ const DECLS = `
2353
+ declare function Body(): any;
2354
+ declare function IsString(): any;
2355
+ declare function IsUUID(a?: any, b?: any): any;
2356
+ declare function MinLength(n: number): any;
2357
+ declare function IsOptional(): any;
2358
+ declare function Type(fn: any): any;
2359
+ declare function Transform(fn: any): any;
2360
+ `;
2361
+ const cases = [
2362
+ {
2363
+ name: "正例·① 跨文件 DTO(属性装饰器 @IsString)",
2364
+ fn: "GroupController.createGroup",
2365
+ files: {
2366
+ "dto.ts": DECLS + `
2367
+ export class CreateGroupDto {
2368
+ @IsString()
2369
+ name: string;
2370
+ }
2371
+ `,
2372
+ "a.ts": `
2373
+ import { CreateGroupDto } from "./dto";
2374
+ export class GroupController {
2375
+ createGroup(@Body() dto: CreateGroupDto) { return this.svc.createGroup(dto); }
2376
+ }
2377
+ `,
2378
+ },
2379
+ want: [MARK],
2380
+ why: "NestJS 最常见形态:校验写在另一个文件的 DTO 类上,函数体内什么都没有",
2381
+ },
2382
+ {
2383
+ name: "正例·② 同文件 DTO",
2384
+ fn: "C.createGroup",
2385
+ files: {
2386
+ "a.ts": DECLS + `
2387
+ export class CreateGroupDto {
2388
+ @IsUUID()
2389
+ id: string;
2390
+ }
2391
+ export class C {
2392
+ createGroup(dto: CreateGroupDto) { return 1; }
2393
+ }
2394
+ `,
2395
+ },
2396
+ want: [MARK],
2397
+ why: "DTO 与消费者同文件(小模块常见)",
2398
+ },
2399
+ {
2400
+ name: "正例·③ @MinLength 也算校验(不只是 Is* 族)",
2401
+ fn: "C.createGroup",
2402
+ files: {
2403
+ "a.ts": DECLS + `
2404
+ export class D {
2405
+ @MinLength(2)
2406
+ name: string;
2407
+ }
2408
+ export class C {
2409
+ createGroup(dto: D) { return 1; }
2410
+ }
2411
+ `,
2412
+ },
2413
+ want: [MARK],
2414
+ why: "R19:分母不只是 Is* 族;MinLength/MaxLength/Matches 都是真校验",
2415
+ },
2416
+ {
2417
+ name: "正例·④ 顶层函数同样覆盖(与 E2 的『仅类/方法』边界不同)",
2418
+ fn: "createGroup",
2419
+ files: {
2420
+ "a.ts": DECLS + `
2421
+ export class D {
2422
+ @IsString()
2423
+ name: string;
2424
+ }
2425
+ export function createGroup(dto: D) { return 1; }
2426
+ `,
2427
+ },
2428
+ want: [MARK],
2429
+ why: "按入参判定,不按函数形态 ⇒ 顶层函数同样接得上(E2 那只覆盖装饰器,本轮不看装饰器)",
2430
+ },
2431
+ {
2432
+ name: "负对照·① DTO 只有 @IsOptional/@Type/@Transform —— 不是校验",
2433
+ fn: "C.createGroup",
2434
+ files: {
2435
+ "a.ts": DECLS + `
2436
+ export class D {
2437
+ @IsOptional()
2438
+ @Type(() => String)
2439
+ @Transform(({ value }) => value)
2440
+ name?: string;
2441
+ }
2442
+ export class C {
2443
+ createGroup(dto: D) { return 1; }
2444
+ }
2445
+ `,
2446
+ },
2447
+ want: [],
2448
+ dontWant: [MARK],
2449
+ why: "可缺省 + 类型转换 ≠ 校验;判成已校验就是假阴性",
2450
+ },
2451
+ {
2452
+ name: "负对照·② 入参是基本类型(不是 DTO 类)",
2453
+ fn: "C.createGroup",
2454
+ files: {
2455
+ "a.ts": `
2456
+ export class C { createGroup(name: string) { return 1; } }
2457
+ `,
2458
+ },
2459
+ want: [],
2460
+ dontWant: [MARK],
2461
+ why: "基线:没有 schema 就不许凭空给出校验证据",
2462
+ },
2463
+ {
2464
+ name: "负对照·③ 无入参",
2465
+ fn: "C.createGroup",
2466
+ files: {
2467
+ "a.ts": DECLS + `
2468
+ export class D { @IsString() name: string; }
2469
+ export class C { createGroup() { return 1; } }
2470
+ `,
2471
+ },
2472
+ want: [],
2473
+ dontWant: [MARK],
2474
+ why: "DTO 存在但没被当入参 ⇒ 该函数的输入没有被校验",
2475
+ },
2476
+ {
2477
+ name: "正对照·⑤ 真实调用不得被挤掉(E1 不回归)",
2478
+ fn: "C.createGroup",
2479
+ files: {
2480
+ "a.ts": DECLS + `
2481
+ export class D { @IsString() name: string; }
2482
+ export class C {
2483
+ createGroup(dto: D) { return this.repo.createGroup(dto); }
2484
+ }
2485
+ `,
2486
+ },
2487
+ want: [MARK, "createGroup"],
2488
+ why: "本轮是**追加**标记;E1 补进来的真实调用必须仍在",
2489
+ },
2490
+ ];
2491
+ for (const c of cases) {
2492
+ (0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
2493
+ const dir = makeProject(c.files);
2494
+ try {
2495
+ const calls = marksFor(dir, c.fn);
2496
+ for (const w of c.want)
2497
+ (0, vitest_1.expect)(calls).toContain(w);
2498
+ for (const d of c.dontWant ?? [])
2499
+ (0, vitest_1.expect)(calls).not.toContain(d);
2500
+ }
2501
+ finally {
2502
+ fs.rmSync(dir, { recursive: true, force: true });
2503
+ }
2504
+ }, 150000);
2505
+ }
2506
+ // ── 规则侧:标记必须真的压得住 Input Validation,且只压这一条 ──
2507
+ // E5(2026-09-23):Input Validation 现在带 requireMarker(须有副作用证据才触发),
2508
+ // 故这几条断言必须显式带上 INPUT_EFFECT —— 它们验证的是「在提取器已产出副作用
2509
+ // 证据的前提下,schema 标记仍能压住本条」,不是「裸名字就该报」。
2510
+ (0, vitest_1.it)("规则侧·① 注入标记后 Input Validation 必须消失", () => {
2511
+ const before = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", INPUT_EFFECT], "createPost");
2512
+ (0, vitest_1.expect)(before.some(v => v.rule === "Input Validation")).toBe(true);
2513
+ const after = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", INPUT_EFFECT, MARK], "createPost");
2514
+ (0, vitest_1.expect)(after.some(v => v.rule === "Input Validation")).toBe(false);
2515
+ });
2516
+ (0, vitest_1.it)("规则侧·② 标记不得压掉别的规则(Authorization 仍在)", () => {
2517
+ const after = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", INPUT_EFFECT, MARK], "createPost");
2518
+ (0, vitest_1.expect)(after.some(v => v.category === "authorization")).toBe(true);
2519
+ });
2520
+ (0, vitest_1.it)("规则侧·③ 标记名不得撞上 Data Integrity 的 safeguard(命名事故回归)", () => {
2521
+ // 曾用的名字 __progmune_validated_input__ 会被
2522
+ // \b(get|find|check|exists|lookup|status|validate|verify)(?:[A-Z]\w*|_\w+)\b 命中,
2523
+ // 凭空压掉 Foreign Key 校验。改名后必须仍报。
2524
+ const withOld = (0, protocol_detector_1.detectSafeguardViolations)(["addChild", "__progmune_validated_input__"], "addChild", undefined, ["parentId"], false);
2525
+ const withNew = (0, protocol_detector_1.detectSafeguardViolations)(["addChild", MARK], "addChild", undefined, ["parentId"], false);
2526
+ (0, vitest_1.expect)(withOld.some(v => v.rule === "Data Integrity (Foreign Key)")).toBe(false);
2527
+ (0, vitest_1.expect)(withNew.some(v => v.rule === "Data Integrity (Foreign Key)")).toBe(true);
2528
+ });
2529
+ });
2530
+ (0, vitest_1.describe)("F:Input Validation 的 trigger 只认函数名(不再被 callee 拖下水)", () => {
2531
+ // 立项依据(2026-09-22):本规则的陈述对象是「这个函数自己是不是内容创建函数」
2532
+ // (violationMessage 原文:"Content creation function does not validate...")。
2533
+ // 此前拿 callee 名字去匹配 trigger,产生两类缺陷,各占一半:
2534
+ // (a) 重复投射 —— 分发器 handleRequest 因调用 createEvent/createRoute 被计一次,
2535
+ // 而那些 callee 自己也在报(实测 100/175,全部 SAFE_DUP)。
2536
+ // (b) 理由错 —— login/register/authenticate 因调用 **createHash**(密码哈希)
2537
+ // 被判成「内容创建函数未校验输入」(69/175)。
2538
+ // 175 条已逐条归类,无一为真阳性;另有 6 条 list/get 查询函数。
2539
+ // E5(2026-09-23):本规则现在带 requireMarker(须有「参数流向外部副作用」的证据
2540
+ // 才触发)。F 组守护的是「trigger 只认函数名、不被 callee 拖下水」——这个命题在
2541
+ // **已有副作用证据**的前提下才成立,故统一带上 INPUT_EFFECT。
2542
+ // 副作用证据本身的行为由下面 E5 组单独钉住(含正反两面与语言限定)。
2543
+ const IV = (calls, own) => (0, protocol_detector_1.detectSafeguardViolations)([...calls, INPUT_EFFECT], own).filter(v => v.rule === "Input Validation");
2544
+ const hit = (calls, own) => IV(calls, own).length > 0;
2545
+ // ── 正例:函数名自己就是创建函数 ⇒ 必须仍然报(本轮不得削弱真阳性) ──
2546
+ (0, vitest_1.it)("正例·① createPost 自身仍报", () => {
2547
+ (0, vitest_1.expect)(hit(["db.save"], "createPost")).toBe(true);
2548
+ });
2549
+ (0, vitest_1.it)("正例·② 类方法 C.createPost 仍报(ownName 取末段)", () => {
2550
+ (0, vitest_1.expect)(hit(["db.save"], "C.createPost")).toBe(true);
2551
+ });
2552
+ (0, vitest_1.it)("正例·③ addComment 仍报", () => {
2553
+ (0, vitest_1.expect)(hit(["db.save"], "addComment")).toBe(true);
2554
+ });
2555
+ (0, vitest_1.it)("正例·④ uploadFile 仍报", () => {
2556
+ (0, vitest_1.expect)(hit(["fs.write"], "uploadFile")).toBe(true);
2557
+ });
2558
+ (0, vitest_1.it)("正例·⑤ 下划线形态 create_post 仍报", () => {
2559
+ (0, vitest_1.expect)(hit(["db.save"], "create_post")).toBe(true);
2560
+ });
2561
+ // ── 负例:靠 callee 触发 ⇒ 必须不再报 ──
2562
+ (0, vitest_1.it)("负例·① 分发器 handleRequest 不再因 createEvent 被计(重复投射)", () => {
2563
+ (0, vitest_1.expect)(hit(["createEvent", "createMetric"], "handleRequest")).toBe(false);
2564
+ });
2565
+ (0, vitest_1.it)("负例·② registerNewUser 不再因 createHash 被判内容创建(密码哈希)", () => {
2566
+ (0, vitest_1.expect)(hit(["createHash", "bcrypt.hash"], "registerNewUser")).toBe(false);
2567
+ });
2568
+ (0, vitest_1.it)("负例·③ doLogin 同上", () => {
2569
+ (0, vitest_1.expect)(hit(["createHash"], "doLogin")).toBe(false);
2570
+ });
2571
+ (0, vitest_1.it)("负例·④ authenticate 同上", () => {
2572
+ (0, vitest_1.expect)(hit(["createHash"], "authenticate")).toBe(false);
2573
+ });
2574
+ (0, vitest_1.it)("负例·⑤ 查询函数 listPosts 不再被报", () => {
2575
+ (0, vitest_1.expect)(hit(["sort", "filter"], "listPosts")).toBe(false);
2576
+ });
2577
+ (0, vitest_1.it)("负例·⑥ ORM/建表类 callee 不再拖累调用者(createQueryBuilder)", () => {
2578
+ (0, vitest_1.expect)(hit(["createQueryBuilder"], "findArticles")).toBe(false);
2579
+ });
2580
+ // ── 边界与回归保护 ──
2581
+ (0, vitest_1.it)("边界·① ownName 缺失时不得误触发(triggerCalls 为空数组)", () => {
2582
+ (0, vitest_1.expect)(hit(["createPost"], "")).toBe(false);
2583
+ });
2584
+ (0, vitest_1.it)("回归·① E3 的 schema 标记仍然压得住(本轮不得破坏 E3)", () => {
2585
+ (0, vitest_1.expect)(hit(["createPost", "__progmune_input_schema__"], "createPost")).toBe(false);
2586
+ (0, vitest_1.expect)(hit(["createPost"], "createPost")).toBe(true);
2587
+ });
2588
+ (0, vitest_1.it)("回归·② 非目标规则不受影响(Authorization 仍在)", () => {
2589
+ const v = (0, protocol_detector_1.detectSafeguardViolations)(["createEvent"], "handleRequest");
2590
+ (0, vitest_1.expect)(v.some(x => x.category === "authorization")).toBe(true);
2591
+ });
2592
+ // ── 已知边界(登记,不断言)──
2593
+ // 函数名不含 create/add/post/upload、但确实是「入口把用户输入交给内容创建」
2594
+ // 的函数(如 handleSubmit 调 commentRepo.create,而 create 不在扫描范围内),
2595
+ // 本轮之后不再报。这是 triggerOwnNameOnly 的固有代价:175 条损失里此类为 0,
2596
+ // real-world 池里亦未发现(27 条触发源全为 ORM/哈希/权限工厂/内部工具)。
2597
+ // 若要补回,需要 `exposed`(web handler)通道 —— 按 R27 先查该通道通不通。
2598
+ });
2599
+ // ═══════════════════════════════════════════════════════════════
2600
+ // E5(2026-09-23):Input Validation 的语义前置条件 —— 参数流向外部副作用
2601
+ //
2602
+ // 立项依据:本规则的 trigger 是纯函数名正则(create|add|post|upload),判据手里
2603
+ // **只有名字**。FP 池实测 75 条里 62 条(83%)完全没有请求入口迹象:
2604
+ // verdaccio ConfigBuilder.addStorage/addLogger(配置装配)、ducktors
2605
+ // createS3/createAzureBlobStorage(工厂)都被要求「必须校验输入」。
2606
+ //
2607
+ // 判据为什么不是「是否 HTTP 入口」:真值集里 4 条已核验真报(docmost
2608
+ // addContributors→redis.sadd、addLabelsToPage→labelRepo.findOrCreate、hedgedoc
2609
+ // ApiTokenService.createToken、hoppscotch AdminService.createATeam)都没有 req
2610
+ // 对象,但参数是从 Controller 透传的外部输入,且确实没校验 ⇒ Service 层不因
2611
+ // 组件边界豁免。区分点在**参数流向**,不在入口形态。
2612
+ //
2613
+ // 量化(probe-body-evidence.ts --from-gold,74 条已标注):
2614
+ // TP 召回 100%(17/17,丢 0)/FP 压制 77.2%(44/57)/precision 23.0%→56.7%
2615
+ // ═══════════════════════════════════════════════════════════════
2616
+ (0, vitest_1.describe)("E5:Input Validation 须有「参数流向外部副作用」的证据才触发", () => {
2617
+ const IV = (calls, own, language) => (0, protocol_detector_1.detectSafeguardViolations)(calls, own, language).filter(v => v.rule === "Input Validation");
2618
+ const hit = (calls, own, language) => IV(calls, own, language).length > 0;
2619
+ // ── 反面:名字像创建函数,但参数不流向任何 sink ⇒ 不该报 ──
2620
+ (0, vitest_1.it)("反例·① 纯内存装配(push 进数组)不再被要求校验输入", () => {
2621
+ (0, vitest_1.expect)(hit(["push", "sort"], "addWidgetToCache")).toBe(false);
2622
+ });
2623
+ (0, vitest_1.it)("反例·② 工厂形态(只构造对象)不再被要求校验输入", () => {
2624
+ // FP 池里 ducktors createS3 / createAzureBlobStorage 即此形态
2625
+ (0, vitest_1.expect)(hit(["Bucket", "Credentials"], "createS3")).toBe(false);
2626
+ });
2627
+ (0, vitest_1.it)("反例·③ 配置装配(写进内部 config 对象)不再被要求校验输入", () => {
2628
+ // FP 池里 verdaccio ConfigBuilder.addStorage / addLogger 即此形态
2629
+ (0, vitest_1.expect)(hit(["assign", "keys"], "addStorage")).toBe(false);
2630
+ });
2631
+ // ── 正面:有副作用证据 ⇒ 仍须报(防止 requireMarker 退化成无条件压制)──
2632
+ (0, vitest_1.it)("正例·① ORM 写入(repo.save)仍报", () => {
2633
+ (0, vitest_1.expect)(hit(["db.save", INPUT_EFFECT], "createWidget")).toBe(true);
2634
+ });
2635
+ (0, vitest_1.it)("正例·② 委托给 service 层(含 upload 动词)仍报", () => {
2636
+ // docmost uploadToDrive 只有 `storageService.upload(...)`,
2637
+ // 动词表漏掉 upload 就会丢这条真报
2638
+ (0, vitest_1.expect)(hit(["storageService.upload", INPUT_EFFECT], "uploadToDrive")).toBe(true);
2639
+ });
2640
+ (0, vitest_1.it)("正例·③ 缓存写入(redis.sadd)仍报 —— Service 层不因无 req 豁免", () => {
2641
+ // docmost CollabHistoryService.addContributors 是已核验真报,没有 req 对象
2642
+ (0, vitest_1.expect)(hit(["redis.sadd", INPUT_EFFECT], "addContributors")).toBe(true);
2643
+ });
2644
+ // ── 语言限定(R42):标记只由 TS 提取器产出,Python 侧不得因此静默归零 ──
2645
+ (0, vitest_1.it)("语言·① TypeScript 缺标记 ⇒ 不报(前置条件生效)", () => {
2646
+ (0, vitest_1.expect)(hit(["db.save"], "createWidget", "typescript")).toBe(false);
2647
+ });
2648
+ (0, vitest_1.it)("语言·② Python 无标记也必须照旧报(不得静默归零)", () => {
2649
+ (0, vitest_1.expect)(hit(["db.save"], "createWidget", "python")).toBe(true);
2650
+ });
2651
+ (0, vitest_1.it)("语言·③ 未传语言时按原语义处理(不引入新的静默失效)", () => {
2652
+ (0, vitest_1.expect)(hit(["db.save", INPUT_EFFECT], "createWidget")).toBe(true);
2653
+ });
2654
+ // ── 与既有抑制机制的关系:两个通道互不覆盖 ──
2655
+ (0, vitest_1.it)("关系·① 有副作用 + 有 schema 标记 ⇒ 仍不报(E3 通道未被本轮破坏)", () => {
2656
+ (0, vitest_1.expect)(hit(["db.save", INPUT_EFFECT, "__progmune_input_schema__"], "createWidget")).toBe(false);
2657
+ });
2658
+ (0, vitest_1.it)("关系·② 有副作用 + 有函数体校验证据 ⇒ 仍不报(E4 通道未被本轮破坏)", () => {
2659
+ (0, vitest_1.expect)(hit(["db.save", INPUT_EFFECT, "__progmune_input_guard__"], "createWidget")).toBe(false);
2660
+ });
2661
+ });
@@ -441,6 +441,234 @@ function authMachineryFromDecorators(decorators) {
441
441
  }
442
442
  return auth && !skip ? [AUTH_MACHINERY_MARKER] : [];
443
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
+ }
512
+ // ═══════════════════════════════════════════════════════════════
513
+ // E4(2026-09-23):函数体里的输入校验证据 ⇒ 注入 __progmune_input_guard__
514
+ //
515
+ // 背景:Input Validation 规则的 safeguard 只看**函数名与调用名**(词形匹配),
516
+ // 看不见函数体。实测 docmost 的 AttachmentController.uploadFile 函数体里明明
517
+ // 写着 `limits:{ fileSize, fields, files }` 与
518
+ // `if (!pageId) throw new BadRequestException('PageId is required')`,
519
+ // 却仍被判「内容创建函数未校验输入」。判据不是不准,是**瞎** ——
520
+ // 缺的是信息,不是语义理解,所以这里是确定性规则而不是概率模型。
521
+ //
522
+ // 判据(逐条来自语料实测,不是拍脑袋词表):
523
+ // A. 上传/写入限制选项:`limits:{ … }` 且块后 140 字符内出现
524
+ // fileSize | maxFileSize | files | fields | parts
525
+ // B. 入参判空即拒:`if (!x) throw <HTTP/校验类异常>`
526
+ // —— 必须是 BadRequest/Validation/Unprocessable/Invalid/Forbidden/Http… 类。
527
+ // 实测两条反例因此被排除:createS3 抛裸 `Error('…required')` 是**环境配置**
528
+ // 校验;AttachmentService.uploadFile 抛 NotFoundException 是**存在性**校验。
529
+ // 两者都不是「对输入内容做校验」。
530
+ //
531
+ // 范围实测:在 FP 池 112 条 Input Validation 违规上,A 与 B 命中集合**完全重合**
532
+ // (均为 docmost 的两个 upload controller),**零命中 verified TP**;
533
+ // 候选 C(校验库调用 validateOrReject/schema.parse)与 D(mime 白名单)
534
+ // 命中数均为 0 ⇒ **不实现**(没有证据支撑的机制不进代码)。
535
+ //
536
+ // 与 E3 同构:AST 预扫 ⇒ key 集合 ⇒ 主循环后统一注入。只进 safeguard,不进 trigger。
537
+ // ═══════════════════════════════════════════════════════════════
538
+ const INPUT_GUARD_MARKER = "__progmune_input_guard__";
539
+ function bodyHasInputGuard(text) {
540
+ if (!text)
541
+ return false;
542
+ // 迭代用的正则必须带 g(非全局正则的 exec 恒返回首个匹配 ⇒ 死循环)。
543
+ // 每次调用新建字面量正则,不复用带 lastIndex 的共享对象。
544
+ for (const m of text.matchAll(/\blimits\s*:\s*\{/gi)) {
545
+ const tail = text.slice(m.index + m[0].length, m.index + m[0].length + 140);
546
+ if (/\b(?:fileSize|maxFileSize|files|fields|parts)\b/i.test(tail))
547
+ return true;
548
+ }
549
+ for (const m of text.matchAll(/\bif\s*\(\s*!\s*[A-Za-z_$][\w$.?]*\s*\)\s*\{?\s*throw\b/g)) {
550
+ const tail = text.slice(m.index + m[0].length, m.index + m[0].length + 120);
551
+ if (/BadRequest|Validation|Unprocessable|Invalid|Forbidden|Http|NotAcceptable|PayloadTooLarge/i.test(tail)) {
552
+ return true;
553
+ }
554
+ }
555
+ return false;
556
+ }
557
+ /**
558
+ * 全项目扫描:函数体内存在输入校验证据的函数集合。
559
+ * key = `${相对路径}|${函数名}`(类方法为 `Class.method`,与 IR 的 f.name 一致)。
560
+ */
561
+ function inputGuardFunctionKeys(project, absRoot) {
562
+ const out = new Set();
563
+ const consider = (name, node, fileRel) => {
564
+ if (!name || !node)
565
+ return;
566
+ if (bodyHasInputGuard(node.getText?.() ?? ""))
567
+ out.add(`${fileRel}|${name}`);
568
+ };
569
+ for (const sf of project.getSourceFiles()) {
570
+ if (sf.getFilePath().includes("node_modules"))
571
+ continue;
572
+ const fileRel = path.relative(absRoot, sf.getFilePath()).split(path.sep).join("/");
573
+ for (const fn of sf.getFunctions())
574
+ consider(fn.getName(), fn, fileRel);
575
+ for (const cls of sf.getClasses()) {
576
+ const cn = cls.getName();
577
+ for (const m of cls.getMethods()) {
578
+ const mn = m.getName();
579
+ consider(cn ? `${cn}.${mn}` : mn, m, fileRel);
580
+ }
581
+ }
582
+ for (const vd of sf.getVariableDeclarations()) {
583
+ const init = vd.getInitializer();
584
+ if (init && (ts_morph_1.Node.isArrowFunction(init) || ts_morph_1.Node.isFunctionExpression(init))) {
585
+ consider(vd.getName(), init, fileRel);
586
+ }
587
+ }
588
+ }
589
+ return out;
590
+ }
591
+ // ═══════════════════════════════════════════════════════════════
592
+ // E5(2026-09-23):参数流向外部副作用的证据 ⇒ 注入 __progmune_input_effect__
593
+ //
594
+ // 背景:Input Validation 规则的判据手里**只有函数名**(trigger 是
595
+ // create|add|post|upload 的名字正则),于是任何叫 add*/create* 的函数都被要求
596
+ // 「必须校验输入」:verdaccio ConfigBuilder.addStorage(配置装配)、ducktors
597
+ // createS3(工厂)都在报。FP 池 75 条里 62 条(83%)完全没有请求入口迹象。
598
+ //
599
+ // 判据来源:不是拍脑袋的词表,是先写探针 blind-benchmark/probe-body-evidence.ts
600
+ // 在**真值集**(fp-gold.jsonl,74 条已标注)上量化出来的:
601
+ // 粗判据 TP 召回 100%(17/17)/ FP 压制 77.2%(44/57)/ precision 23%→56.7%
602
+ // 精化判据(要求副作用调用吃到参数)FP 压制 91.2% 但丢 4 条真报 ⇒ 弃
603
+ // ⇒ 本文件里的正则与探针里的 EFFECT 表**逐字一致**,改一处必须改另一处
604
+ // (否则实测口径与代码口径不可比,R38)。
605
+ //
606
+ // 判据不是「是否 HTTP 入口」:见 protocol-detector.ts 侧注释,那条判据会压掉
607
+ // 4 条已核验真报。这里问的是「参数流向」,不是「入口形态」。
608
+ //
609
+ // 与 E3/E4 同构:AST 预扫 ⇒ key 集合 ⇒ 主循环后统一注入。
610
+ // ═══════════════════════════════════════════════════════════════
611
+ const INPUT_EFFECT_MARKER = "__progmune_input_effect__";
612
+ // 与 probe-body-evidence.ts 的 EFFECT 表一致(S1-S7)
613
+ const INPUT_EFFECT_PATTERNS = [
614
+ // S1 ORM/repository 写入
615
+ /\b(?:save|create|update|insert|upsert|persist|remove|deleteMany|delete|findOrCreate|upsertMany|createMany|updateMany)\s*\(/,
616
+ // S2 缓存/Redis 写入
617
+ /\b(?:sadd|hset|hmset|lpush|rpush|setex|zadd|incr|append)\s*\(/,
618
+ // S3 HTTP 出站 / 消息投递
619
+ /\b(?:fetch|axios|request|got|superagent)\s*[.(]|\bhttps?\.\s*(?:request|get|post)\s*\(|\b(?:publish|emit|send|dispatch)\s*\(/,
620
+ // S4 文件系统写入
621
+ /\b(?:writeFile|writeFileSync|appendFile|createWriteStream|mkdir|unlink|rm|rename|copyFile)\s*\(/,
622
+ // S5 委托给 repo/service/storage 层。**必须大小写不敏感**:webshape_E 语料写的是
623
+ // `repo.saveBanner(file)`(小写前缀 + 带后缀动词),大写前缀版两条件都不满足
624
+ // ⇒ 副作用漏判 ⇒ 该报的被压掉,闸门当场转红 3 条。
625
+ // 动词表含 upload:docmost uploadToDrive 只有 `this.storageService.upload(...)`,
626
+ // 漏一个动词就丢一条真报。
627
+ /\b\w*(?:repo|repository|service|storage|dao|manager|gateway|client|provider)\s*\.\s*(?:create|add|save|update|insert|upsert|write|store|persist|set|upload|put|send|publish|remove|delete)[A-Za-z0-9_]*\s*\(/i,
628
+ // S6 prisma / knex 写入
629
+ /\bprisma\s*\.\s*\w+\s*\.\s*(?:create|update|upsert|delete|createMany|updateMany)\s*\(|\b(?:knex|db|connection|tx|trx)\s*\(\s*['"]\w+['"]\s*\)\s*\.\s*(?:insert|update|del)\s*\(/,
630
+ // S7 Kysely 链式(trx.insertInto(...).values(...).execute())
631
+ /\.\s*(?:insertInto|updateTable|deleteFrom|insert|update|del|delete)\s*\(/,
632
+ ];
633
+ function bodyHasExternalEffect(text) {
634
+ if (!text)
635
+ return false;
636
+ return INPUT_EFFECT_PATTERNS.some((re) => re.test(text));
637
+ }
638
+ /**
639
+ * 全项目扫描:函数体内存在「参数流向外部副作用」证据的函数集合。
640
+ * key = `${相对路径}|${函数名}`(类方法为 `Class.method`,与 IR 的 f.name 一致)。
641
+ */
642
+ function inputEffectFunctionKeys(project, absRoot) {
643
+ const out = new Set();
644
+ const consider = (name, node, fileRel) => {
645
+ if (!name || !node)
646
+ return;
647
+ if (bodyHasExternalEffect(node.getText?.() ?? ""))
648
+ out.add(`${fileRel}|${name}`);
649
+ };
650
+ for (const sf of project.getSourceFiles()) {
651
+ if (sf.getFilePath().includes("node_modules"))
652
+ continue;
653
+ const fileRel = path.relative(absRoot, sf.getFilePath()).split(path.sep).join("/");
654
+ for (const fn of sf.getFunctions())
655
+ consider(fn.getName(), fn, fileRel);
656
+ for (const cls of sf.getClasses()) {
657
+ const cn = cls.getName();
658
+ for (const m of cls.getMethods()) {
659
+ const mn = m.getName();
660
+ consider(cn ? `${cn}.${mn}` : mn, m, fileRel);
661
+ }
662
+ }
663
+ for (const vd of sf.getVariableDeclarations()) {
664
+ const init = vd.getInitializer();
665
+ if (init && (ts_morph_1.Node.isArrowFunction(init) || ts_morph_1.Node.isFunctionExpression(init))) {
666
+ consider(vd.getName(), init, fileRel);
667
+ }
668
+ }
669
+ }
670
+ return out;
671
+ }
444
672
  /**
445
673
  * 标识符是否「看起来像基目录」—— G-A2 的定案依据。
446
674
  *
@@ -2803,6 +3031,26 @@ function _extractSingleProject(absRoot, tsconfigPath) {
2803
3031
  if (!fs.existsSync(tsconfigPath)) {
2804
3032
  project.addSourceFilesAtPaths(path.join(absRoot, "**/*.ts"));
2805
3033
  }
3034
+ // ═══════════════════════════════════════════════════════════════
3035
+ // 零源文件兜底(2026-09-22):solution 风格 tsconfig 会静默产出 0 函数
3036
+ //
3037
+ // 根 tsconfig 是「容器」时(`"files": []` + `"include": []` + `references`
3038
+ // 指向 tsconfig.app.json 等),ts-morph 按它加载会得到 **0 个源文件** ⇒
3039
+ // 0 个函数 ⇒ 0 条违规,**且不报错、不告警**。
3040
+ // 这比崩溃危险得多:调用方会把「0 函数」读成「这个仓库很干净」——
3041
+ // 实测踩到:FP 池 6 个切片里 gothinkster-node-express-realworld-example-app
3042
+ // 一直以 functions=0 参与统计(切片只保留了根 tsconfig.json,它引用的
3043
+ // tsconfig.app.json / tsconfig.spec.json 不在切片里,references 因此全被过滤掉)。
3044
+ //
3045
+ // 只在「确实一个源文件都没有」时才兜底 ⇒ 现有工程全部不受影响(它们是
3046
+ // `include: ["src"]` 的正常 tsconfig,走不到这里)。兜底同时打 stderr,
3047
+ // 让这类切片不可能再悄悄以「0 违规」混入统计。
3048
+ // ═══════════════════════════════════════════════════════════════
3049
+ if (!project.getSourceFiles().some((sf) => !sf.getFilePath().includes("node_modules"))) {
3050
+ console.error(`⚠️ [extract-ir] ${absRoot}:tsconfig 未纳入任何源文件(solution 风格 / 引用缺失)→ 回退为全目录 glob`);
3051
+ project.addSourceFilesAtPaths(path.join(absRoot, "**/*.ts"));
3052
+ project.addSourceFilesAtPaths(path.join(absRoot, "**/*.tsx"));
3053
+ }
2806
3054
  const funcs = [];
2807
3055
  // 标记增强的共享状态(2026-09-15 内联重构):
2808
3056
  // sinkParams = 项目方法名 → 方法体内流入文件 sink 的形参下标 + 方法条目;
@@ -2969,6 +3217,41 @@ function _extractSingleProject(absRoot, tsconfigPath) {
2969
3217
  }
2970
3218
  }
2971
3219
  }
3220
+ const validatedDtoClasses = validatedDtoClassNames(project);
3221
+ // ── E3(2026-09-22):入参已按 schema 校验 ⇒ 注入 __progmune_input_schema__
3222
+ // 放在主循环之后统一应用,覆盖全部函数形态(函数声明 / 箭头 / 类方法),
3223
+ // 与 pendingMethodMarks 的跨函数标记同一处收口。
3224
+ for (const f of funcs) {
3225
+ if (!f.params || f.params.length === 0)
3226
+ continue;
3227
+ const hit = f.params.some((p) => p.type && validatedDtoClasses.has(p.type));
3228
+ if (!hit)
3229
+ continue;
3230
+ f.calls = f.calls || [];
3231
+ if (!f.calls.includes(INPUT_SCHEMA_MARKER))
3232
+ f.calls.push(INPUT_SCHEMA_MARKER);
3233
+ }
3234
+ // ── E4(2026-09-23):函数体里有输入校验证据 ⇒ 注入 __progmune_input_guard__
3235
+ // 与 E3 同一处收口、同一套形态(函数声明 / 类方法 / 变量箭头)。
3236
+ const inputGuardKeys = inputGuardFunctionKeys(project, absRoot);
3237
+ for (const f of funcs) {
3238
+ if (!inputGuardKeys.has(`${f.file}|${f.name}`))
3239
+ continue;
3240
+ f.calls = f.calls || [];
3241
+ if (!f.calls.includes(INPUT_GUARD_MARKER))
3242
+ f.calls.push(INPUT_GUARD_MARKER);
3243
+ }
3244
+ // ── E5(2026-09-23):参数流向外部副作用的证据 ⇒ 注入 __progmune_input_effect__
3245
+ // 与 E3/E4 同一处收口、同一套形态(函数声明 / 类方法 / 变量箭头)。
3246
+ // Input Validation 规则据此做 requireMarker(无副作用证据 ⇒ 不要求输入校验)。
3247
+ const inputEffectKeys = inputEffectFunctionKeys(project, absRoot);
3248
+ for (const f of funcs) {
3249
+ if (!inputEffectKeys.has(`${f.file}|${f.name}`))
3250
+ continue;
3251
+ f.calls = f.calls || [];
3252
+ if (!f.calls.includes(INPUT_EFFECT_MARKER))
3253
+ f.calls.push(INPUT_EFFECT_MARKER);
3254
+ }
2972
3255
  // ═══════════════════════════════════════════════════════════════
2973
3256
  // Phase 5: Dynamic external function resolution
2974
3257
  // Replaces hardcoded knownExternals
@@ -429,9 +429,46 @@ 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,
440
+ // E5(2026-09-23):语义前置条件 —— 只有「参数确实流向外部副作用」才谈得上
441
+ // 该不该校验。此前 trigger 是纯函数名正则(create|add|post|upload),判据手里
442
+ // **只有名字**:verdaccio 的 ConfigBuilder.addStorage/addLogger(纯配置装配)、
443
+ // ducktors 的 createS3/createAzureBlobStorage(工厂)都被要求「必须校验输入」,
444
+ // 而它们根本不接外部输入。FP 池实测:75 条里 62 条(83%)完全没有请求入口迹象。
445
+ //
446
+ // 为什么是「副作用」而不是「是否 HTTP 入口」(R41:先分缺信息 vs 缺语义):
447
+ // 「非入口 ⇒ 不报」这个更直觉的判据**有毒** —— 真值集里 4 条已核验 TP
448
+ // (docmost addContributors→redis.sadd、addLabelsToPage→labelRepo.findOrCreate、
449
+ // hedgedoc ApiTokenService.createToken、hoppscotch AdminService.createATeam)
450
+ // 都没有 req 对象,但参数是从 Controller 透传的外部输入,且确实没有校验。
451
+ // ⇒ Service 层方法不因「组件边界」获得豁免。区分点在**参数流向**,不在入口形态。
452
+ //
453
+ // 量化(blind-benchmark/probe-body-evidence.ts --from-gold,74 条已标注真值):
454
+ // TP 召回 100%(17/17,丢 0 条真报)/FP 压制 77.2%(44/57)/
455
+ // precision 23.0% → 56.7%/净收益 44 条。多数类基线 77.0%。
456
+ // 「要求副作用调用吃到参数」的精化版 FP 压制虽达 91.2%,但会丢 4 条真报,
457
+ // 漏报比误报贵 ⇒ 取粗判据。
458
+ requireMarker: "__progmune_input_effect__",
459
+ // 该标记只由 TS 提取器产出(Python 提取器不产出任何 __progmune_* 标记),
460
+ // 故只在 TS/JS 上强制,Python 侧保持原有行为,避免静默归零(R42)。
461
+ requireMarkerLanguages: ["typescript", "javascript"],
433
462
  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" },
463
+ // E3(2026-09-22):__progmune_input_schema__ —— 提取器在「入参类型是带校验
464
+ // 装饰器的 DTO 类」时产出(NestJS 把校验写在 DTO 属性装饰器上,词形匹配永远
465
+ // 看不到)。本规则此前不接受任何 __progmune_* 标记 ⇒ 该证据通道是断的。
466
+ // 注意只加在**抑制位**(safeguard),不进 trigger:新通道只能做减法。
467
+ // E4(2026-09-23):__progmune_input_guard__ —— 提取器在函数体内找到输入校验
468
+ // 证据时产出(limits:{fileSize…} / if(!x) throw BadRequest…)。此前规则只看
469
+ // 函数名与调用名,函数体里的校验看不见 ⇒ docmost 两个 upload controller
470
+ // 明明有校验仍被判「未校验」。与 E3 同源:只加在抑制位,不进 trigger。
471
+ { 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__|__progmune_input_guard__/i, label: "input_validation" },
435
472
  ],
436
473
  violationMessage: "Content creation function does not validate or sanitize input. XSS, injection, and oversized content possible.",
437
474
  conceptMissing: ["InputSanitization", "ContentValidation", "SizeLimit"],
@@ -1093,7 +1130,7 @@ function identifierParse(name) {
1093
1130
  const words = [];
1094
1131
  for (const part of parts) {
1095
1132
  // Split camelCase/PascalCase: "registerNewUser" → ["register", "New", "User"]
1096
- const camelWords = part.replace(/([a-z])([A-Z])/g, "$1$2").split("");
1133
+ const camelWords = part.replace(/([a-z])([A-Z])/g, "$1\x00$2").split("\x00");
1097
1134
  for (const w of camelWords) {
1098
1135
  if (w.length > 0)
1099
1136
  words.push(w);
@@ -1129,13 +1166,24 @@ function detectSafeguardViolations(calls, enclosingFuncName, language, params, e
1129
1166
  : SAFEGUARD_RULES;
1130
1167
  for (const rule of activeRules) {
1131
1168
  // Check if trigger matches
1132
- const triggerCalls = rule.triggerCallsOnly ? rawCalls : effectiveCalls;
1169
+ const triggerCalls = rule.triggerOwnNameOnly
1170
+ ? (ownName ? [ownName] : [])
1171
+ : rule.triggerCallsOnly
1172
+ ? rawCalls
1173
+ : effectiveCalls;
1133
1174
  const triggerMatch = triggerCalls.some(c => rule.trigger.test(c));
1134
1175
  if (!triggerMatch)
1135
1176
  continue;
1136
- // Semantic precondition marker (extractor-emitted)
1137
- if (rule.requireMarker && !effectiveCalls.includes(rule.requireMarker))
1138
- continue;
1177
+ // Semantic precondition marker (extractor-emitted).
1178
+ // E5(2026-09-23):只在能产出该标记的语言上强制。不限定语言的话,
1179
+ // Python 侧(提取器不产出任何 __progmune_* 标记)会静默归零——
1180
+ // 而 TS 侧所有闸门都看不见这种退化(R42)。
1181
+ if (rule.requireMarker) {
1182
+ const enforce = !rule.requireMarkerLanguages
1183
+ || (language ? rule.requireMarkerLanguages.includes(language) : true);
1184
+ if (enforce && !effectiveCalls.includes(rule.requireMarker))
1185
+ continue;
1186
+ }
1139
1187
  // Skip authorization rules for auth functions — they ARE the auth
1140
1188
  if (isAuthFunction && rule.category === "authorization")
1141
1189
  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.49";
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.49",
3
+ "version": "3.7.52",
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/",