progmune-runtime 3.7.51 → 3.7.53

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,62 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.53] — 2026-09-26
4
+
5
+ ### 授权族误报压降收口 + 三处可见性基建(§35-§46 合并发布)
6
+
7
+ **可见性基建(§35-§37,只读探针,不进产品路径)**:跨函数调用链视图原型;采样单位从
8
+ 「文件」换成「入口的调用闭包」;入口根可达性视图(「谁会走到我」)。三者共同解决
9
+ **切片看不出跨函数证据**的问题——后续每一轮压降都靠它们回答「这一刀在哪个语料上被触发」。
10
+
11
+ **两处判据进主干(§38、§39)**:
12
+ - 对象级授权判据从「动词白名单」换成「组合式谓语」,不再靠 `get/list/create` 这类词根猜语义
13
+ - D 类自指排除:把**鉴权机制自身**也识别为 auth function(此前机制代码被当成未鉴权的业务代码)
14
+
15
+ **授权族压降(§40-§46)**:
16
+ - §42 补 `webshape_G`(17 条授权形态)——**此前盲测在授权命名轴上根本没有差分能力**,
17
+ 改任何判据都看不出效果;补形后项目 117→118、Authorization 族 938→947
18
+ - §44 `paramGated`:**标量参数不是调用者身份**。函数参数名出现 `userId/ownerId` 但类型是
19
+ 裸 string/number 的,不再当鉴权证据(`classifyParamType` 三分类);真实池 337→333、
20
+ 盲测 2887→2618,269 条损失全为 dispatcher 重复投影
21
+ - §45 修同族 safeguard 词表漂移(Unauthenticated Access 缺 `getSession/validateSession/
22
+ verifyToken`)——漂移的表现是**同样形状的一条报、一条不报**;盲测 2618→2610
23
+ - §46 ① `credential_issuance`:`create_*_token` 语义是**签发**不是**校验**,从 `auth_check`
24
+ 移出单列(行为不变,消除误改诱因);② `owner_self_assigned`:只认**绑定形态**
25
+ `{ownerId: user.id}`,不认实参形态 `f(x, user.id)`(真实池 3/3 FP vs 合成 1/1 TP,
26
+ 真值相反 ⇒ R71),同刻比对压掉 197 条全 FP、真漏洞零伤
27
+
28
+ **三条被数据否决的(不落地)**:§43「容器后缀=Service」(造不出反例,R66)、
29
+ §45「triggerOwnNameOnly」(R66 二次命中)、§46③「safeguard 只看调用不看是否检查返回结果」
30
+ (新增 26 条全是已知 dispatcher 误报,收益 0,R72)。
31
+
32
+ **方法学累计**:R66-R73。其中两条是**基线陷阱**——R67(冻结母本不是同刻基线,误报压制数
33
+ 48→真实 4)、R73(上一次的 results 也不是同刻基线,误报压制数 12→真实 197,低估 16 倍)。
34
+ 正确口径只有一个:**摘掉这一刀、其余不动、重跑**。次要:R68(dist 不是 src 的同步副本,
35
+ 陈旧是静默的)、R69(同族词表要定期做集合 diff)、R70(label 必须与语义一致)。
36
+
37
+ **门**:tsc 0 错 / webshape 66-0 / taintpath 160-0 / fr-007 pre5·post0 + fr-016 pre7·post0 /
38
+ 定向 9 文件 329 条全绿 / 反向验证:② 摘刀 197 条全部回来、① 摘刀 fixture 转红、
39
+ REVERSE-VERIFY 残留 0 / 真实切片 LOST 0·ADDED 0
40
+
41
+ ## [3.7.52] — 2026-09-23
42
+
43
+ ### 语料补形 + 两处基础设施修复 + E4/E5 两个确定性通道(§26-§31 合并发布)
44
+
45
+ **语料补形(§26,R23 家族的处置而非第五条注记)**:`webshape_A..F` 六族钉进 generated/——NestJS 装饰器/DTO 校验/类方法入口/分发器/上传校验证据/副作用 sink 形状;`check-webshape.ts` 闸门(默认实时提取、语料缺失即硬失败);期望表由生成器落盘(R7);补形后 exposed 通道两变体的离线重放得以全量验证——增量召回 2 条真报 vs 70 条已知误报 ⇒ **exposed 通道按 R33 判不做**(做完也不划算,理由写死进设计稿 §25)。
46
+
47
+ **基础设施两修(§27)**:
48
+ - 死切片复活:solution 风格仓库的根 tsconfig 是容器 ⇒ 提取器 0 源文件静默通过(gothinkster 37 文件 0 函数污染所有分母)——修复=零源文件兜底回退 `**/*.ts` + stderr 告警(115 generated 项目 0 误触发)+ 抓取器保留 tsconfig 变体
49
+ - language 门:TS 扫描一直传 undefined ⇒ activeRules 退化「全部规则」,Python/C 专属规则套在 TS 上(池少 5 条 Context Manager Usage)——batch-scan/fp-pool-scan 补 "typescript"
50
+ - R36:产出 0 的环节必须自证扫到了东西;R37 判据冲突检测(当场逼出 L7 虚高 3 条真报);R38 口径可比性;R39 gold 分层+多数类基线;R40 外部服务先无效凭据验证
51
+
52
+ **E4(§30):判据瞎,不是判据不准**(R41 缺信息 vs 缺语义)——`inputGuardFunctionKeys` 预扫函数体校验证据(limits:{…}+HTTP 异常抛掷)⇒ `__progmune_input_guard__`(只进抑制位);webshape_E 5 条正反钉死;池 −5 条全部逐条确证(3 条在探针预测外——探针漏箭头常量形态)
53
+
54
+ **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 条照常报
55
+
56
+ **方法学累计**:R34(反向验证期望须由期望表推导,本轮第三次同形踩坑——推导逻辑应工具化)、R41-R43;fp-gold.jsonl 真值集入库(verified 53 条/12.8%——启发式不能当 gold 的实证);jev 实验台(无 key,401 确认端点,ECE/多数类基线硬指标,绝不进扫描路径)
57
+
58
+ **门**: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 ✓
59
+
3
60
  ## [3.7.51] — 2026-09-22
4
61
 
5
62
  ### Input Validation 的 trigger 只认函数名:不再被 callee 拖下水(F 轮)
@@ -2343,6 +2343,10 @@ export class Uploader {
2343
2343
  // 标记不得顺带压掉别的规则(Data Integrity 曾因标记名含 validate 被误压,
2344
2344
  // 见 extract-ir.ts 里标记命名的说明 ⇒ 这是本组专门要守的)。
2345
2345
  // ═══════════════════════════════════════════════════════════════
2346
+ /** E5(2026-09-23):提取器在「参数流向外部副作用」时注入的语义前置标记。
2347
+ * Input Validation 规则据此 requireMarker —— 名字叫 create/add 但根本不接外部
2348
+ * 输入的函数(配置装配、工厂)不该被要求「必须校验输入」。 */
2349
+ const INPUT_EFFECT = "__progmune_input_effect__";
2346
2350
  (0, vitest_1.describe)("E3:DTO 的 schema 校验必须对 Input Validation 可见", () => {
2347
2351
  const MARK = "__progmune_input_schema__";
2348
2352
  const DECLS = `
@@ -2500,14 +2504,17 @@ export class C {
2500
2504
  }, 150000);
2501
2505
  }
2502
2506
  // ── 规则侧:标记必须真的压得住 Input Validation,且只压这一条 ──
2507
+ // E5(2026-09-23):Input Validation 现在带 requireMarker(须有副作用证据才触发),
2508
+ // 故这几条断言必须显式带上 INPUT_EFFECT —— 它们验证的是「在提取器已产出副作用
2509
+ // 证据的前提下,schema 标记仍能压住本条」,不是「裸名字就该报」。
2503
2510
  (0, vitest_1.it)("规则侧·① 注入标记后 Input Validation 必须消失", () => {
2504
- const before = (0, protocol_detector_1.detectSafeguardViolations)(["createPost"], "createPost");
2511
+ const before = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", INPUT_EFFECT], "createPost");
2505
2512
  (0, vitest_1.expect)(before.some(v => v.rule === "Input Validation")).toBe(true);
2506
- const after = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", MARK], "createPost");
2513
+ const after = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", INPUT_EFFECT, MARK], "createPost");
2507
2514
  (0, vitest_1.expect)(after.some(v => v.rule === "Input Validation")).toBe(false);
2508
2515
  });
2509
2516
  (0, vitest_1.it)("规则侧·② 标记不得压掉别的规则(Authorization 仍在)", () => {
2510
- const after = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", MARK], "createPost");
2517
+ const after = (0, protocol_detector_1.detectSafeguardViolations)(["createPost", INPUT_EFFECT, MARK], "createPost");
2511
2518
  (0, vitest_1.expect)(after.some(v => v.category === "authorization")).toBe(true);
2512
2519
  });
2513
2520
  (0, vitest_1.it)("规则侧·③ 标记名不得撞上 Data Integrity 的 safeguard(命名事故回归)", () => {
@@ -2529,7 +2536,11 @@ export class C {
2529
2536
  // (b) 理由错 —— login/register/authenticate 因调用 **createHash**(密码哈希)
2530
2537
  // 被判成「内容创建函数未校验输入」(69/175)。
2531
2538
  // 175 条已逐条归类,无一为真阳性;另有 6 条 list/get 查询函数。
2532
- const IV = (calls, own) => (0, protocol_detector_1.detectSafeguardViolations)(calls, own).filter(v => v.rule === "Input Validation");
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");
2533
2544
  const hit = (calls, own) => IV(calls, own).length > 0;
2534
2545
  // ── 正例:函数名自己就是创建函数 ⇒ 必须仍然报(本轮不得削弱真阳性) ──
2535
2546
  (0, vitest_1.it)("正例·① createPost 自身仍报", () => {
@@ -2585,3 +2596,66 @@ export class C {
2585
2596
  // real-world 池里亦未发现(27 条触发源全为 ORM/哈希/权限工厂/内部工具)。
2586
2597
  // 若要补回,需要 `exposed`(web handler)通道 —— 按 R27 先查该通道通不通。
2587
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
+ });
@@ -379,6 +379,19 @@ function extractDirectCalls(func, preText) {
379
379
  if (/ownerId\s*[!=]==?|authorId\s*[!=]==?|createdBy\s*[!=]==?|\.owner\s*[!=]==?|userId\s*[!=]==?/.test(text)) {
380
380
  calls.push("__progmune_ownership_checked__");
381
381
  }
382
+ // §46 ②:新建资源的属主被**赋值为当前主体**(`{ userId: user.id }` / `ownerId: currentUser.id`)。
383
+ // 这类「创建」函数的被操作对象就是主体自己,不存在「改别人的东西」这回事 ⇒
384
+ // Ownership Check 不该报(合成语料上 10/10 是 FP:createTransfer / createRefund /
385
+ // uploadFile / placeOrder…)。调用名接口看不到对象字面量里的赋值,只能靠标记。
386
+ //
387
+ // ⚠ 只认 `<属主字段>: <主体>.id` 这一种形态,**不认** `<调用>(..., user.id, ...)`:
388
+ // 后者是「主体标识作实参」,实测同形状存在相反真值 ——
389
+ // `deletePAT(id, user.uid)`(已按属主过滤,FP)vs `deleteWidget(id)` 里
390
+ // `canSendEmail(user.id)`(user.id 与 deleteWidget 无关,真 TP)⇒ 按 R66 不得落地。
391
+ // 形态:`<属主字段>: <主体变量>.<标识属性>` (对象字面量内的赋值,不是查询比较)
392
+ if (/\b(?:userId|ownerId|authorId|createdBy|user_id|owner_id|author_id|created_by)\s*:\s*(?:user|currentUser|authUser|me|self|actor|principal|adminUser|(?:request|req|session|ctx|context)\.user)\s*\.\s*(?:id|uid|_id|userId|email|username)\b/i.test(text)) {
393
+ calls.push("__progmune_owner_self_assigned__");
394
+ }
382
395
  return [...new Set(calls)];
383
396
  }
384
397
  // ═══════════════════════════════════════════════════════════════
@@ -509,6 +522,166 @@ function validatedDtoClassNames(project) {
509
522
  }
510
523
  return out;
511
524
  }
525
+ // ═══════════════════════════════════════════════════════════════
526
+ // E4(2026-09-23):函数体里的输入校验证据 ⇒ 注入 __progmune_input_guard__
527
+ //
528
+ // 背景:Input Validation 规则的 safeguard 只看**函数名与调用名**(词形匹配),
529
+ // 看不见函数体。实测 docmost 的 AttachmentController.uploadFile 函数体里明明
530
+ // 写着 `limits:{ fileSize, fields, files }` 与
531
+ // `if (!pageId) throw new BadRequestException('PageId is required')`,
532
+ // 却仍被判「内容创建函数未校验输入」。判据不是不准,是**瞎** ——
533
+ // 缺的是信息,不是语义理解,所以这里是确定性规则而不是概率模型。
534
+ //
535
+ // 判据(逐条来自语料实测,不是拍脑袋词表):
536
+ // A. 上传/写入限制选项:`limits:{ … }` 且块后 140 字符内出现
537
+ // fileSize | maxFileSize | files | fields | parts
538
+ // B. 入参判空即拒:`if (!x) throw <HTTP/校验类异常>`
539
+ // —— 必须是 BadRequest/Validation/Unprocessable/Invalid/Forbidden/Http… 类。
540
+ // 实测两条反例因此被排除:createS3 抛裸 `Error('…required')` 是**环境配置**
541
+ // 校验;AttachmentService.uploadFile 抛 NotFoundException 是**存在性**校验。
542
+ // 两者都不是「对输入内容做校验」。
543
+ //
544
+ // 范围实测:在 FP 池 112 条 Input Validation 违规上,A 与 B 命中集合**完全重合**
545
+ // (均为 docmost 的两个 upload controller),**零命中 verified TP**;
546
+ // 候选 C(校验库调用 validateOrReject/schema.parse)与 D(mime 白名单)
547
+ // 命中数均为 0 ⇒ **不实现**(没有证据支撑的机制不进代码)。
548
+ //
549
+ // 与 E3 同构:AST 预扫 ⇒ key 集合 ⇒ 主循环后统一注入。只进 safeguard,不进 trigger。
550
+ // ═══════════════════════════════════════════════════════════════
551
+ const INPUT_GUARD_MARKER = "__progmune_input_guard__";
552
+ function bodyHasInputGuard(text) {
553
+ if (!text)
554
+ return false;
555
+ // 迭代用的正则必须带 g(非全局正则的 exec 恒返回首个匹配 ⇒ 死循环)。
556
+ // 每次调用新建字面量正则,不复用带 lastIndex 的共享对象。
557
+ for (const m of text.matchAll(/\blimits\s*:\s*\{/gi)) {
558
+ const tail = text.slice(m.index + m[0].length, m.index + m[0].length + 140);
559
+ if (/\b(?:fileSize|maxFileSize|files|fields|parts)\b/i.test(tail))
560
+ return true;
561
+ }
562
+ for (const m of text.matchAll(/\bif\s*\(\s*!\s*[A-Za-z_$][\w$.?]*\s*\)\s*\{?\s*throw\b/g)) {
563
+ const tail = text.slice(m.index + m[0].length, m.index + m[0].length + 120);
564
+ if (/BadRequest|Validation|Unprocessable|Invalid|Forbidden|Http|NotAcceptable|PayloadTooLarge/i.test(tail)) {
565
+ return true;
566
+ }
567
+ }
568
+ return false;
569
+ }
570
+ /**
571
+ * 全项目扫描:函数体内存在输入校验证据的函数集合。
572
+ * key = `${相对路径}|${函数名}`(类方法为 `Class.method`,与 IR 的 f.name 一致)。
573
+ */
574
+ function inputGuardFunctionKeys(project, absRoot) {
575
+ const out = new Set();
576
+ const consider = (name, node, fileRel) => {
577
+ if (!name || !node)
578
+ return;
579
+ if (bodyHasInputGuard(node.getText?.() ?? ""))
580
+ out.add(`${fileRel}|${name}`);
581
+ };
582
+ for (const sf of project.getSourceFiles()) {
583
+ if (sf.getFilePath().includes("node_modules"))
584
+ continue;
585
+ const fileRel = path.relative(absRoot, sf.getFilePath()).split(path.sep).join("/");
586
+ for (const fn of sf.getFunctions())
587
+ consider(fn.getName(), fn, fileRel);
588
+ for (const cls of sf.getClasses()) {
589
+ const cn = cls.getName();
590
+ for (const m of cls.getMethods()) {
591
+ const mn = m.getName();
592
+ consider(cn ? `${cn}.${mn}` : mn, m, fileRel);
593
+ }
594
+ }
595
+ for (const vd of sf.getVariableDeclarations()) {
596
+ const init = vd.getInitializer();
597
+ if (init && (ts_morph_1.Node.isArrowFunction(init) || ts_morph_1.Node.isFunctionExpression(init))) {
598
+ consider(vd.getName(), init, fileRel);
599
+ }
600
+ }
601
+ }
602
+ return out;
603
+ }
604
+ // ═══════════════════════════════════════════════════════════════
605
+ // E5(2026-09-23):参数流向外部副作用的证据 ⇒ 注入 __progmune_input_effect__
606
+ //
607
+ // 背景:Input Validation 规则的判据手里**只有函数名**(trigger 是
608
+ // create|add|post|upload 的名字正则),于是任何叫 add*/create* 的函数都被要求
609
+ // 「必须校验输入」:verdaccio ConfigBuilder.addStorage(配置装配)、ducktors
610
+ // createS3(工厂)都在报。FP 池 75 条里 62 条(83%)完全没有请求入口迹象。
611
+ //
612
+ // 判据来源:不是拍脑袋的词表,是先写探针 blind-benchmark/probe-body-evidence.ts
613
+ // 在**真值集**(fp-gold.jsonl,74 条已标注)上量化出来的:
614
+ // 粗判据 TP 召回 100%(17/17)/ FP 压制 77.2%(44/57)/ precision 23%→56.7%
615
+ // 精化判据(要求副作用调用吃到参数)FP 压制 91.2% 但丢 4 条真报 ⇒ 弃
616
+ // ⇒ 本文件里的正则与探针里的 EFFECT 表**逐字一致**,改一处必须改另一处
617
+ // (否则实测口径与代码口径不可比,R38)。
618
+ //
619
+ // 判据不是「是否 HTTP 入口」:见 protocol-detector.ts 侧注释,那条判据会压掉
620
+ // 4 条已核验真报。这里问的是「参数流向」,不是「入口形态」。
621
+ //
622
+ // 与 E3/E4 同构:AST 预扫 ⇒ key 集合 ⇒ 主循环后统一注入。
623
+ // ═══════════════════════════════════════════════════════════════
624
+ const INPUT_EFFECT_MARKER = "__progmune_input_effect__";
625
+ // 与 probe-body-evidence.ts 的 EFFECT 表一致(S1-S7)
626
+ const INPUT_EFFECT_PATTERNS = [
627
+ // S1 ORM/repository 写入
628
+ /\b(?:save|create|update|insert|upsert|persist|remove|deleteMany|delete|findOrCreate|upsertMany|createMany|updateMany)\s*\(/,
629
+ // S2 缓存/Redis 写入
630
+ /\b(?:sadd|hset|hmset|lpush|rpush|setex|zadd|incr|append)\s*\(/,
631
+ // S3 HTTP 出站 / 消息投递
632
+ /\b(?:fetch|axios|request|got|superagent)\s*[.(]|\bhttps?\.\s*(?:request|get|post)\s*\(|\b(?:publish|emit|send|dispatch)\s*\(/,
633
+ // S4 文件系统写入
634
+ /\b(?:writeFile|writeFileSync|appendFile|createWriteStream|mkdir|unlink|rm|rename|copyFile)\s*\(/,
635
+ // S5 委托给 repo/service/storage 层。**必须大小写不敏感**:webshape_E 语料写的是
636
+ // `repo.saveBanner(file)`(小写前缀 + 带后缀动词),大写前缀版两条件都不满足
637
+ // ⇒ 副作用漏判 ⇒ 该报的被压掉,闸门当场转红 3 条。
638
+ // 动词表含 upload:docmost uploadToDrive 只有 `this.storageService.upload(...)`,
639
+ // 漏一个动词就丢一条真报。
640
+ /\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,
641
+ // S6 prisma / knex 写入
642
+ /\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*\(/,
643
+ // S7 Kysely 链式(trx.insertInto(...).values(...).execute())
644
+ /\.\s*(?:insertInto|updateTable|deleteFrom|insert|update|del|delete)\s*\(/,
645
+ ];
646
+ function bodyHasExternalEffect(text) {
647
+ if (!text)
648
+ return false;
649
+ return INPUT_EFFECT_PATTERNS.some((re) => re.test(text));
650
+ }
651
+ /**
652
+ * 全项目扫描:函数体内存在「参数流向外部副作用」证据的函数集合。
653
+ * key = `${相对路径}|${函数名}`(类方法为 `Class.method`,与 IR 的 f.name 一致)。
654
+ */
655
+ function inputEffectFunctionKeys(project, absRoot) {
656
+ const out = new Set();
657
+ const consider = (name, node, fileRel) => {
658
+ if (!name || !node)
659
+ return;
660
+ if (bodyHasExternalEffect(node.getText?.() ?? ""))
661
+ out.add(`${fileRel}|${name}`);
662
+ };
663
+ for (const sf of project.getSourceFiles()) {
664
+ if (sf.getFilePath().includes("node_modules"))
665
+ continue;
666
+ const fileRel = path.relative(absRoot, sf.getFilePath()).split(path.sep).join("/");
667
+ for (const fn of sf.getFunctions())
668
+ consider(fn.getName(), fn, fileRel);
669
+ for (const cls of sf.getClasses()) {
670
+ const cn = cls.getName();
671
+ for (const m of cls.getMethods()) {
672
+ const mn = m.getName();
673
+ consider(cn ? `${cn}.${mn}` : mn, m, fileRel);
674
+ }
675
+ }
676
+ for (const vd of sf.getVariableDeclarations()) {
677
+ const init = vd.getInitializer();
678
+ if (init && (ts_morph_1.Node.isArrowFunction(init) || ts_morph_1.Node.isFunctionExpression(init))) {
679
+ consider(vd.getName(), init, fileRel);
680
+ }
681
+ }
682
+ }
683
+ return out;
684
+ }
512
685
  /**
513
686
  * 标识符是否「看起来像基目录」—— G-A2 的定案依据。
514
687
  *
@@ -2871,6 +3044,26 @@ function _extractSingleProject(absRoot, tsconfigPath) {
2871
3044
  if (!fs.existsSync(tsconfigPath)) {
2872
3045
  project.addSourceFilesAtPaths(path.join(absRoot, "**/*.ts"));
2873
3046
  }
3047
+ // ═══════════════════════════════════════════════════════════════
3048
+ // 零源文件兜底(2026-09-22):solution 风格 tsconfig 会静默产出 0 函数
3049
+ //
3050
+ // 根 tsconfig 是「容器」时(`"files": []` + `"include": []` + `references`
3051
+ // 指向 tsconfig.app.json 等),ts-morph 按它加载会得到 **0 个源文件** ⇒
3052
+ // 0 个函数 ⇒ 0 条违规,**且不报错、不告警**。
3053
+ // 这比崩溃危险得多:调用方会把「0 函数」读成「这个仓库很干净」——
3054
+ // 实测踩到:FP 池 6 个切片里 gothinkster-node-express-realworld-example-app
3055
+ // 一直以 functions=0 参与统计(切片只保留了根 tsconfig.json,它引用的
3056
+ // tsconfig.app.json / tsconfig.spec.json 不在切片里,references 因此全被过滤掉)。
3057
+ //
3058
+ // 只在「确实一个源文件都没有」时才兜底 ⇒ 现有工程全部不受影响(它们是
3059
+ // `include: ["src"]` 的正常 tsconfig,走不到这里)。兜底同时打 stderr,
3060
+ // 让这类切片不可能再悄悄以「0 违规」混入统计。
3061
+ // ═══════════════════════════════════════════════════════════════
3062
+ if (!project.getSourceFiles().some((sf) => !sf.getFilePath().includes("node_modules"))) {
3063
+ console.error(`⚠️ [extract-ir] ${absRoot}:tsconfig 未纳入任何源文件(solution 风格 / 引用缺失)→ 回退为全目录 glob`);
3064
+ project.addSourceFilesAtPaths(path.join(absRoot, "**/*.ts"));
3065
+ project.addSourceFilesAtPaths(path.join(absRoot, "**/*.tsx"));
3066
+ }
2874
3067
  const funcs = [];
2875
3068
  // 标记增强的共享状态(2026-09-15 内联重构):
2876
3069
  // sinkParams = 项目方法名 → 方法体内流入文件 sink 的形参下标 + 方法条目;
@@ -3051,6 +3244,27 @@ function _extractSingleProject(absRoot, tsconfigPath) {
3051
3244
  if (!f.calls.includes(INPUT_SCHEMA_MARKER))
3052
3245
  f.calls.push(INPUT_SCHEMA_MARKER);
3053
3246
  }
3247
+ // ── E4(2026-09-23):函数体里有输入校验证据 ⇒ 注入 __progmune_input_guard__
3248
+ // 与 E3 同一处收口、同一套形态(函数声明 / 类方法 / 变量箭头)。
3249
+ const inputGuardKeys = inputGuardFunctionKeys(project, absRoot);
3250
+ for (const f of funcs) {
3251
+ if (!inputGuardKeys.has(`${f.file}|${f.name}`))
3252
+ continue;
3253
+ f.calls = f.calls || [];
3254
+ if (!f.calls.includes(INPUT_GUARD_MARKER))
3255
+ f.calls.push(INPUT_GUARD_MARKER);
3256
+ }
3257
+ // ── E5(2026-09-23):参数流向外部副作用的证据 ⇒ 注入 __progmune_input_effect__
3258
+ // 与 E3/E4 同一处收口、同一套形态(函数声明 / 类方法 / 变量箭头)。
3259
+ // Input Validation 规则据此做 requireMarker(无副作用证据 ⇒ 不要求输入校验)。
3260
+ const inputEffectKeys = inputEffectFunctionKeys(project, absRoot);
3261
+ for (const f of funcs) {
3262
+ if (!inputEffectKeys.has(`${f.file}|${f.name}`))
3263
+ continue;
3264
+ f.calls = f.calls || [];
3265
+ if (!f.calls.includes(INPUT_EFFECT_MARKER))
3266
+ f.calls.push(INPUT_EFFECT_MARKER);
3267
+ }
3054
3268
  // ═══════════════════════════════════════════════════════════════
3055
3269
  // Phase 5: Dynamic external function resolution
3056
3270
  // Replaces hardcoded knownExternals
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * §41 E1(2026-09-25):token 内嵌 acronym 的**局部**补认 —— 正/反向 fixture
5
+ *
6
+ * 背景:C5 原案是「改主干 identifierParse 让 JWTPayload 拆出 JWT」。
7
+ * `split-probe.ts` 实测否决了它(详见 src 里 ACRONYM_PREFIX 的注释):
8
+ * 803 个去重标识符里只有 16 个受影响、新增 22 词中仅 2 个(JWT/OIDC)是机制类,
9
+ * 其余是 `OAuthToken→Auth`、`createATeam→A,Team` 这类噪声;
10
+ * 而 identifierParse 还供给 triggerParsedWords ⇒ 动它等于改全局触发面。
11
+ * ⇒ 最终落地的是**只在 isAuthMachineryName 的对象侧**补认 acronym。
12
+ *
13
+ * 为什么必须有这里的 fixture(同 §40,R56):改动方向是「压掉告警」,
14
+ * 而 generated/ 合成语料里**没有** `verifyJWTPayload` 这类形态 ⇒
15
+ * 盲测零漂移很可能是零触发。**这里是唯一能证明这段判据活着的证据。**
16
+ */
17
+ const vitest_1 = require("vitest");
18
+ const protocol_detector_1 = require("./protocol-detector");
19
+ /** 只关心 authorization 类目(本轮只影响这一类目) */
20
+ const authz = (fnName, calls = ["deleteSpace", "findOne"]) => (0, protocol_detector_1.detectSafeguardViolations)(calls, fnName, "typescript", ["user", "id"], true).filter((v) => /^Authorization/i.test(v.rule));
21
+ (0, vitest_1.describe)("§41 E1 正向:acronym 卡在函数名的机制对象里(应该被认成鉴权机制)", () => {
22
+ (0, vitest_1.it)("verifyJWTPayload —— §40 剩余池待救的那条(verdaccio packages/auth/src/utils.ts)", () => {
23
+ (0, vitest_1.expect)(authz("verifyJWTPayload")).toHaveLength(0);
24
+ });
25
+ (0, vitest_1.it)("verifyJWTPayload 带触发 trigger 也一样压", () => {
26
+ (0, vitest_1.expect)(authz("verifyJWTPayload", ["getTarball", "readPackage"])).toHaveLength(0);
27
+ });
28
+ (0, vitest_1.it)("JWTRemote 类托管的 jwt 机制方法", () => {
29
+ (0, vitest_1.expect)(authz("JwtRemote.getJWTRemote")).toHaveLength(0);
30
+ });
31
+ (0, vitest_1.it)("类限定形式 AvailabilityService.verifyJWTPayload", () => {
32
+ (0, vitest_1.expect)(authz("AvailabilityService.verifyJWTPayload")).toHaveLength(0);
33
+ });
34
+ (0, vitest_1.it)("createJWTToken —— 唯有 E1 才救得回来的一条(反向验证揪出来的错误假设)", () => {
35
+ // ⚠ 这条曾经被我标成「BASE 就命中」,是**错的**:
36
+ // identifierParse("createJWTToken") → ["create","JWTToken"]
37
+ // ⇒ 对象侧只拿到 "jwttoken",既 ≠ jwt 也 ≠ token ⇒ BASE **不命中**。
38
+ // 反向验证(把 machineryWords 退回 identifierParse)让它转红才发现。
39
+ // 教训同 R59:凭印象断言既有行为,与口径漂移是同一类错误。
40
+ (0, vitest_1.expect)(authz("createJWTToken")).toHaveLength(0);
41
+ });
42
+ (0, vitest_1.it)("⚠ verifyOIDCToken 不收 —— oidc 只在**容器侧**名词表,**不在方法对象词表**", () => {
43
+ // §39.8 的收窄是有意的:对象侧一旦放宽到 oauth/oidc/saml,
44
+ // `ApiTokensController.deleteToken` 这类凭据 CRUD 会被误压(V-D 教训)。
45
+ // 剩余池里没有待救的 verifyOIDCToken 形态 ⇒ 按 R50 不为此扩词表。
46
+ (0, vitest_1.expect)(authz("verifyOIDCToken").length).toBeGreaterThan(0);
47
+ });
48
+ (0, vitest_1.it)("verifySSLSignature 不在机制对象词表 ⇒ 仍报(词表不含 ssl,别幻想)", () => {
49
+ (0, vitest_1.expect)(authz("verifySSLSignature").length).toBeGreaterThan(0);
50
+ });
51
+ });
52
+ /**
53
+ * `tokenAcronyms` 的直接单元测试 —— 端到端 fixture 覆盖不到这两条约束,
54
+ * 因为**对象侧 acronym 与 BASE 的重叠度极高**(凡 acronym 等于 jwt/token/…
55
+ * 的,BASE 下独立 token 本来也就命中了),端到端分不出「约束生效」与
56
+ * 「恰好 BASE 也命中」。剩下来真正会被 `${2,}` 拦住的,只有单字母情形。
57
+ */
58
+ (0, vitest_1.describe)("§41 tokenAcronyms 表驱动:两条约束(≥2 字母 / 贪婪回溯)", () => {
59
+ const cases = [
60
+ ["JWTPayload", ["JWT"]],
61
+ ["OIDCStrategy", ["OIDC"]], // ← 贪婪:非贪婪会停在 "OID",救不了这条
62
+ ["HTTPErr", ["HTTP"]],
63
+ ["CLIError", ["CLI"]],
64
+ ["JWTToken", ["JWT"]],
65
+ // 以下必须为空:单字母 / 纯 PascalCase / 已有独立词
66
+ ["OAuth", []], // ← 放宽到 {1,} 就会产出 "O"
67
+ ["ATeam", []],
68
+ ["YElement", []],
69
+ ["Token", []],
70
+ ["Payload", []],
71
+ ["payload", []],
72
+ ["JWTPayloadService", ["JWT"]], // 边界:最后一个大写归后词 ⇒ 不会吃成 "JWTP"
73
+ ];
74
+ for (const [input, want] of cases) {
75
+ (0, vitest_1.it)(`${input} → ${JSON.stringify(want)}`, () => {
76
+ (0, vitest_1.expect)((0, protocol_detector_1.tokenAcronyms)(input)).toEqual(want);
77
+ });
78
+ }
79
+ });
80
+ (0, vitest_1.describe)("§41 E1 反向:不得把非机制名字误认成机制(R57 宽变体约束)", () => {
81
+ (0, vitest_1.it)("⚠ validateOAuthToken —— BASE 就会命中(validate × Token),与 acronym 无关", () => {
82
+ // 存在意义:端到端根本测不出「OAuth 被拆出 Auth」—— 因为这条 BASE 下也压。
83
+ // 若有人把 `{2,}` 放宽成 `{1,}`,`OAuth` → "O",而 "O" 不在任何词表里
84
+ // ⇒ 端到端行为完全不变,**回归无从发现**。单字母噪声只有在表驱动层才可见。
85
+ (0, vitest_1.expect)(authz("validateOAuthToken")).toHaveLength(0);
86
+ });
87
+ (0, vitest_1.it)("⚠ createATeam 不得拆出 Team 之外的机制词", () => {
88
+ (0, vitest_1.expect)(authz("AdminService.createATeam").length).toBeGreaterThan(0);
89
+ });
90
+ (0, vitest_1.it)("⚠ throwHTTPErr 不得因 HTTP 被当成机制", () => {
91
+ (0, vitest_1.expect)(authz("throwHTTPErr").length).toBeGreaterThan(0);
92
+ });
93
+ (0, vitest_1.it)("⚠ createCLIErrorResponse 不得因 CLI/Error 被当成机制", () => {
94
+ (0, vitest_1.expect)(authz("createCLIErrorResponse").length).toBeGreaterThan(0);
95
+ });
96
+ (0, vitest_1.it)("⚠ 纯 PascalCase 词不得被再切一遍(Token → T/oken 之类的臆想不生效)", () => {
97
+ (0, vitest_1.expect)(authz("deleteExpiredToken").length).toBeGreaterThan(0);
98
+ });
99
+ (0, vitest_1.it)("⚠ 动词缺失 ⇒ acronym 对象不算(只有对象没有机制动词)", () => {
100
+ // mw 必须同时含 VERB × OBJECT;JWTPayload 单独不是机制
101
+ (0, vitest_1.expect)(authz("logJWTPayload").length).toBeGreaterThan(0);
102
+ });
103
+ /**
104
+ * 最要紧的一条反向:**凭据增删改**即使名字里含 JWT 也不能压 —— 与 §39 V-D、
105
+ * §40 W4 同源教训。这条守的是「约束在于组合,不在于 acronym 本身」。
106
+ */
107
+ (0, vitest_1.it)("⚠️ createJWTToken 是依赖 E1 的哨兵 ⇒ 反向验证时必须转红", () => {
108
+ // §41 反向验证实测(把 machineryWords 退回 identifierParse):
109
+ // 全套 76 条里 **6 条**转红,其余 70 条(§38/§39/§40)纹丝不动
110
+ // ⇒ E1 的影响面被严格锁在「机制对象侧 acronym」这一格,没有横向污染。
111
+ // 这条 earlier 被错写成「GENERATE 类动词…既有口径一致」—— 与 R59 同源的
112
+ // 「凭印象断言既有行为」,已据反向验证的实测更正。
113
+ (0, vitest_1.expect)(authz("createJWTToken")).toHaveLength(0);
114
+ });
115
+ (0, vitest_1.it)("⚠ 与 §40 C4 的分工:容器=Auth 且方法受保护动词开头 ⇒ 不压", () => {
116
+ (0, vitest_1.expect)(authz("Auth.changeJWTToken").length).toBeGreaterThan(0);
117
+ });
118
+ });
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * §45:Authorization (Unauthenticated Access) 的 auth_check 词表漂移修复。
5
+ *
6
+ * 病灶:同一族的 Unauthenticated Mutation 认 `getSession` / `validateSession` / `verifyToken`,
7
+ * Unauthenticated Access 只认 `getSessionUser`,**不认裸 `getSession`**。
8
+ * 于是「先取会话再操作」的读类函数被判成「未认证即可读」—— 真实案例 filestorage 的
9
+ * uploadFile / deleteFile / createFolder / createShareLink 四条(体里都有
10
+ * `const user = getSession(token); if (!user) return null;`)。
11
+ *
12
+ * 修复:把三个词补进 Access 规则的 auth_check(与 Mutation 对齐)。
13
+ * 不做:三个 mint 类词(create_access_token / create_jwt_token / create_refresh_token)——
14
+ * 「签发令牌」不是「校验调用者」,语义不同,不在本轮(见设计文档 §45.6)。
15
+ */
16
+ const vitest_1 = require("vitest");
17
+ const protocol_detector_1 = require("./protocol-detector");
18
+ const ACC = "Authorization (Unauthenticated Access)";
19
+ const MUT = "Authorization (Unauthenticated Mutation)";
20
+ /** 单 LANGUAGE=typescript;paramGate 用 user:User 放行(类型是非标量领域类型) */
21
+ function dsv(calls, name) {
22
+ return (0, protocol_detector_1.detectSafeguardViolations)(calls, name, "typescript", ["user"], false, ["User"]);
23
+ }
24
+ const has = (calls, name, rule) => dsv(calls, name).some((v) => v.rule === rule);
25
+ (0, vitest_1.describe)("§45 getSession 漂移:读类函数已取会话 ⇒ 不再报 Unauthenticated Access", () => {
26
+ (0, vitest_1.it)("getSession 应被认作认证检查(filestorage uploadFile 原型)", () => {
27
+ (0, vitest_1.expect)(has(["getSession", "join", "writeFileSync"], "uploadFile", ACC)).toBe(false);
28
+ });
29
+ (0, vitest_1.it)("validateSession 应被认作认证检查", () => {
30
+ (0, vitest_1.expect)(has(["validateSession", "find"], "listDocuments", ACC)).toBe(false);
31
+ });
32
+ (0, vitest_1.it)("verifyToken 应被认作认证检查", () => {
33
+ (0, vitest_1.expect)(has(["verifyToken", "find"], "getDocument", ACC)).toBe(false);
34
+ });
35
+ (0, vitest_1.it)("类限定调用形态 this.auth.getSession() 也成立(identifierParse 拆词后命中)", () => {
36
+ (0, vitest_1.expect)(has(["this", "auth", "getSession", "find"], "deleteFile", ACC)).toBe(false);
37
+ });
38
+ });
39
+ (0, vitest_1.describe)("§45 反向对照:真正的未认证读取必须仍然报", () => {
40
+ (0, vitest_1.it)("getDocuments 无任何认证调用 ⇒ 仍报", () => {
41
+ (0, vitest_1.expect)(has(["find", "filter"], "getDocuments", ACC)).toBe(true);
42
+ });
43
+ (0, vitest_1.it)("listReports 无任何认证调用 ⇒ 仍报", () => {
44
+ (0, vitest_1.expect)(has(["filter", "map"], "listReports", ACC)).toBe(true);
45
+ });
46
+ (0, vitest_1.it)("downloadExport 无任何认证调用 ⇒ 仍报", () => {
47
+ (0, vitest_1.expect)(has(["readFileSync", "pipe"], "downloadExport", ACC)).toBe(true);
48
+ });
49
+ (0, vitest_1.it)("getSessionUser 之外的无关 get* 不构成认证证据(getConfig 类)", () => {
50
+ // getConfig 走 excludePatterns;这里用 getUniqueId 确保普通取值不算检查
51
+ (0, vitest_1.expect)(has(["getUniqueId", "find"], "listAuditLogs", ACC)).toBe(true);
52
+ });
53
+ });
54
+ (0, vitest_1.describe)("§45 与 Mutation 规则的对称性:同一形态两条规则都不报", () => {
55
+ const calls = ["getSession", "find"];
56
+ (0, vitest_1.it)("读侧", () => (0, vitest_1.expect)(has(calls, "listInvoices", ACC)).toBe(false));
57
+ (0, vitest_1.it)("写侧", () => (0, vitest_1.expect)(has(["getSession", "push"], "createInvoice", MUT)).toBe(false));
58
+ });
59
+ (0, vitest_1.describe)("§45 已知未覆盖范围(登记为债,不是 bug)", () => {
60
+ (0, vitest_1.it)("⚠ 只看「是否调用」getSession,不看「是否检查其结果」—— 这是既有宽松", () => {
61
+ // `const u = getSession(t); return data;` (不检查 u)仍会被判为已认证。
62
+ // 单函数视角拿不到数据流,本轮不修;见设计文档 §45.6。
63
+ (0, vitest_1.expect)(has(["getSession", "return"], "getEverything", ACC)).toBe(false);
64
+ });
65
+ });