progmune-runtime 3.7.4 → 3.7.6

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,48 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.6] — 2026-08-28
4
+
5
+ ### C 生产级路径收官:金标 5/5 + 采纳案例 + 正则层噪声治理
6
+
7
+ - **金标 4/5(libssh 回调分发认证)**(`demo-real-c-libssh-cb/` + `REALWORLD_C_V6.md`):真实 `samplesshd-cb.c`(359 行)逐字——现代回调分发 API(`ssh_server_callbacks_struct` 的 `auth_password_function` / `channel_open_request_session_function`,决策记录指定的最优模块)。2 注解 → APPROVED 82、真实代码 0 FP、植入 `cb_session_no_auth` 精确定位;认证完成跃迁在 libssh 内部 + main 循环条件(L3 边界),establish 由演示层 wrapper 表达(V5 wrapper 模式)
8
+ - **金标 5/5(uftpd 数据传送授权)**:采纳项目第二个协议——真实 `do_RETR`/`do_STOR` + 2 注解(pre AUTHENTICATED → post AUTHORIZED,镜像 check_resource_ownership 语义)→ 真实代码 0 FP、`ftp_transfer_no_login` 精确定位(fixPath → `establish_login`);金标累计 5/5 全部 ~2-3 注解/协议
9
+ - **发现 G5(规则面缺口入册)**:SSG 状态机 per-namespace——内置 `check_resource_ownership`(data_integrity,pre=[AUTHENTICATED])永远不可满足(AUTHENTICATED 在 auth 命名空间,src/tests 全仓库零引用)——规则面设计缺口,非引擎缺陷
10
+ - **正则层噪声治理(V5 发现 4 落地)**:`PLAINTEXT_AUTH_WITHOUT_TLS` 加 `languages` 门控排除 C(真实语料证据 3 FP / 0 TP:libssh 演示 1 + uftpd 采纳案例 2——FTP/SSH 应用层本就明文,Web/TLS 语义对 C 无意义);SSH 主机密钥规则保留全语言(libssh 演示 1 TP)。`checkSpecificViolations` 加 `language` 参数、engine 传 `ctx.language`、5 个回归测试;**双零漂移**:Python 盲测 v1.2 64 违规(仅时间戳差)、C 应用级金标 F1=95.7% 不变;uftpd 重扫 PLAINTEXT ×2 消失、libssh 演示 FP 消失 TP 保留
11
+ - **标签升级已拍板**:C「⚠️ 研究」→「✅ 注解驱动协议验证(Beta)」——README 双语、覆盖矩阵双语、c-language-status、CLAUDE.md 同步翻新;能力边界如实保留(未注解不检测、TLS 级无覆盖)
12
+
13
+ ### C 定位拍板「注解驱动」+ 库边界机制 + 精度修复
14
+
15
+ - **定位拍板**(Decision record 定稿):C = 注解驱动协议验证(研究级)——未注解自动检测 0 TP/3 FP,两条自动检测桥(C 库别名注册表、方言解析器)不排期;文档翻转:README 双语 + 覆盖矩阵双语 C 行「⚠️ 注解驱动(研究级)」
16
+ - **库边界机制(孵化器)**:两层——项目原语注解(`@progmune`,不迁移)+ **库边界别名**(`.progmune_aliases.json` → 共享表 `c-aliases.json`,跨项目迁移)。新增:`scripts/c-annotate.js`(注释块模板 + 别名条目建议脚手架)、`scripts/c-alias-propose.js`(别名校验 + 回写提案 + 人工确认门);引擎 `loadProtocolRules` 合并共享表 **confirmed** 条目(proposed 不生效、不覆盖全局/项目别名、规则不存在跳过);`c-aliases.json` 入 npm 包
17
+ - **库边界演示**(`demo-real-c-libssh/` + `REALWORLD_C_V3.md`):真实 libssh authentication.c 逐字 + 1 别名 + 1 注解 → SSG 层 0 误报、植入 missing-auth 精确定位;正则防护层同跑:SSH_NO_HOST_KEY_CHECK 为**真发现**(示例确实不验证主机密钥)、PLAINTEXT_AUTH_WITHOUT_TLS 为 FP(Web 语义规则误映射 SSH,如实记录)
18
+ - **发现**:多机制认证重试循环的状态机语义缺口(verify 类规则 pre 不可重入——全机制别名映射会误报正常重试流,候选:可重试标记);fixPath 反向映射(规则名 → 库调用名)候选
19
+ - **陷阱修复(DSH 复测发现)**:`evaluateTrust` 的注解合并依赖 ir.json 写盘,而自动提取此前仅 TS/JS 生效——C/Python 项目直接调用时注解静默失效。现在按语言分派提取器自动写盘(C 走合并形态;TS/JS 路径零变化)+ 回归测试;提取器改静态导入(vitest 下 lazy require 的 CJS 互操作不可靠——该分支从未被测试触发过)
20
+ - **精度修复**:真实语料 24 FP → 3 FP(-87.5%)
21
+
22
+ ### C 精度修复:真实语料 24 FP → 3 FP(-87.5%)
23
+
24
+ - **Strategy 1 normalized 门控**:Windows API(ReadFile/WriteFile/DeleteFile)经 CamelCase→snake_case 规范化撞上内置规则名是 11/24 FP 的主导源——normalized 分支套用词段门控同款 projectFunctions 门(外部 API 不桥接;注解桥接不受影响——注解原语必为项目函数;原始名精确匹配不限门控)
25
+ - **endState 直接调用溯源**:12/24 FP 是 nginx 回调式生命周期(open 在内联 helper 链、close 在指针注册的回调里——L3 级不可见)——`CallSequence.directCalls` + `validateSequenceWithSSG` 的 `entryDirectCalls` 参数,仅当资源获取调用是入口的直接调用时报告 endState(helper 获取不归因入口)
26
+ - **结果**:真实语料四仓库 24 flags → **3 flags**(nginx 14→0、redis 0、libssh 1、openssl 2;残留 3 条是词段桥接按设计命中项目函数)。双回归门全过:Python 盲测 v1.2 64 违规零漂移(仅时间戳差异)、C 应用级金标 **TP 11/FP 1/FN 0 → F1=95.7% 不变**(leak_file 的 endState TP 保留)、引擎相关套件 120/120
27
+ - **战略记录**:C 产品定位(自动检测 vs 注解驱动)待拍板——写入 `docs/c-language-status.md` Decision record;证据偏向注解驱动(精度修复后未注解自动检测仍 0 TP),金标扩量延后至定位拍板
28
+
29
+ ## [3.7.5] — 2026-08-27
30
+
31
+ ### C 真实语料验证 + 注解驱动演示 + 引擎修复(DSH 双轮评审合入)
32
+
33
+ - **修复(DSH):单行指针返回函数系统性漏提取**——`char *foo(`/`SSL *foo(` 类定义被 `(?:^|\s)` 锚点漏掉(名字前是 `*` 非空白);改为 lookbehind `(?<![a-zA-Z0-9_])` + 回归测试。openssl 等指针密集型仓库补回 ~1,500 函数(如 openssl 15,539 含指针修复与表面过滤的净效果)
34
+ - **修复:P4.6 展开兆级序列**(DSH 实测 openssl 单序列可达 1M+ 调用、全扫描 15–25 分钟)——`buildCallSequences` 预算制展开(`MAX_SEQUENCE_CALLS=2000` 可注入,入口自身调用按序优先、超预算即停,`CallSequence.truncated` 标记,不去重——重复调用对状态机有语义);openssl 全扫描 **15–25 分钟 → 222s**。零漂移前置实测:Python 盲测语料最大序列 23、TS 自身 IR 最大 824(预算零影响),Python 盲测复跑 64 违规不变。**截断是诚实的召回边界**(超大序列尾部违规不可见,非静默回归)
35
+ - **真实语料四仓库**(libssh/redis/nginx/openssl):24 flags 全部人工标注 FP(0 TP)——误报类别稳定(OS API 桥接 11、回调 endState、包装器词段、跨函数窗口),keyword 白名单方向的观测前提已达标待决策;稳定指标 = 黄金函数恢复率 97–100%
36
+
37
+ - **真实 C 语料验证 v1**(`blind-benchmark/scan-real-c.ts` + `REALWORLD_C_V1.md`):生产管线扫 libssh/redis/nginx——表面过滤后 16 flags 逐条人工标注全 FP(真实误报率观测:标记精确率 0%);「命名鸿沟」发现(exact-name 0 次触发,合成金标 95.7% F1 全靠按名+注解命中);误报源分类:回调生命周期 endState 12 / OS API 关键词桥接 3 / 跨函数窗口 1
38
+ - **提取器非生产表面过滤**(对齐 `tools/extract_ir.py` Python 先例):`collectCFiles` 跳过 `tests/test/examples/docs/docs_src/scripts/deps/vendor/third_party` 目录与 `test_*.c`/`*_test.c`——libssh 63→1、redis 1→0 条 flags;C 金标恢复率零漂移(97/97/89/98/100/99)
39
+ - **注解驱动真实项目演示**(`demo-real-c-redis/` + `REALWORLD_C_V2.md`):真实 redis acl.c 代码 + 3 条注解 → 合法流 APPROVED 85 零误报、植入 missing-auth-check 精确定位;标注成本 ~3 注解/协议——**注解驱动是 C 生产化的现实形态,可行性已验证**
40
+ - **引擎修复①(CamelCase 注解规则不可触达)**:注解合并同步注册 normalized 形态(加性,snake_case 注解零变化)
41
+ - **引擎修复②(注解合并晚于序列构建)**:P4.5 合并移到 `extractCallSequencesFromProject` 之前——有函数体的注解原语不再被内联掉、post 状态生效;与盲测 harness 语义对齐
42
+ - **引擎修复③(fixPath 输出真实函数名)**:`StateAnnotation.displayName` 机制——注解合并记录真实函数名,BFS 展开项目原语优先(stable sort 零漂移)+ 渲染映射;修复建议从通用规则名(`verify_token`)变为项目真实函数(`checkPasswordBasedAuth`),sdk 修复解析直接插入真实调用。三处边界如实记录:establish 赋值状态机不可见(L4 不投入)、模块认证 hook 在状态机外、单条 medium 违规不翻转 APPROVED(决策阈值层独立议题)
43
+ - **零漂移验证**:Python 协议盲测 v1.2 复跑 64 违规(报告仅时间戳差异);引擎相关套件 113/113 + 2 个新回归测试(tests/trust/engine.test.ts)
44
+ - **如实记录**:C 语言状态标签维持「研究」;不修项(按评估决策)——establish 赋值不可见(L4 不投入)、medium 违规不翻转 APPROVED(累计扣分设计逻辑,非缺陷)
45
+
3
46
  ## [3.7.4] — 2026-08-26
4
47
 
5
48
  ### 新增:C 语言 IR 提取(注册表第三语言)
@@ -7,7 +50,7 @@
7
50
  - **`src/extract-ir-c.ts`**:纯 TS C 提取器(无子进程桥、无原生依赖)——函数签名(含多行、`static`/`inline`/`__attribute__`、指针/数组/函数指针参数)、调用列表(成员调用取 `->`/`.` 后的调用名,`goto` 合成 `goto_<label>`)、`@progmune`/`@protocol` 注解与 `@purpose/@tags/@requires/@produces/@useWhen/@inputs/@outputs` 文档标签(C 注释块镜像 Python 装饰器语法);注释/字符串感知的括号计数(修复 v2 提取器已知缺口,未改动 `sequence-extractor`——C 金标基准管线保持不动)
8
51
  - **`LANGUAGE_EXTRACTORS` 注册 `c`**(detect `.c`/`.h`,extract `extractIRC`)——agent 循环、execute() 的 ir.json 写入与 MCP 自动生效;C 项目从纯正则回退切换到 IR-first 序列验证 + SSG 状态机,C 函数名进入词段匹配门控(仅项目函数);协议行与 protocols.json 规则名(`verify_password` 等)按名命中
9
52
  - **端到端验证**:临时 C 项目上 extractProjectIR → evaluateTrust 走通——4 条植入违规全部精确定位(内置 auth×2 / db×1 + 自定义 pay 命名空间注解×1),合法链零误报(NEEDS_REVIEW 72 分);**应用级 C 金标 v1**(`blind-benchmark/scan-protocol-c-app.ts`,镜像 Python 盲测方法学):10 clean × 7 违规 → **P=87.5% / R=100% / F1=93.3%**(唯一 FP 为跨函数窗口边界,与 Python 盲测 T2×S5 同类)
10
- - **规模化提取**(`blind-benchmark/scan-protocol-c.ts`):6 个 vendored 仓库(curl 4192 / libssh 3689 / nginx 2970 / openssl 14394 / nghttp2 1274 / redis 9590 函数)秒级提取,黄金函数恢复率 8199%;旧 TLS 级金标上 SSG 命中 0/38(口径差异:SSG 无 TLS 规则,如实记录);nginx 3 FP 为 `ngx_*` 前缀包装器撞词段匹配(引擎层问题,记录待议,未动 SSG 桥避免 TS/Python 漂移)
53
+ - **规模化提取**(`blind-benchmark/scan-protocol-c.ts`):6 个 vendored 仓库(curl 5068 / libssh 3989 / nginx 3199 / openssl 15896 / nghttp2 1315 / redis 10170 函数,3.7.4 发布时代码状态——后续版本口径见 3.7.5)秒级提取,黄金函数恢复率 89100%;旧 TLS 级金标上 SSG 命中 0/38(口径差异:SSG 无 TLS 规则,如实记录);nginx 3 FP 为 `ngx_*` 前缀包装器撞词段匹配(引擎层问题,记录待议,未动 SSG 桥避免 TS/Python 漂移)
11
54
  - **修复:签名正则指数级回溯**——v2 风格类型 token 循环对 `name = ssh_userauth_kbdint_getname(...)` 类行穷举标识符切分(44 字符缓冲 ~11s),改为候选迭代(跳过关键字/类型名候选,返回类型从缓冲区前缀推导);libssh 提取 >15min(病态)→ 1.8s,回归测试已加
12
55
  - **评审修复轮(detect/extract 口径、死代码、TU 绑定等)**:①`hasSourceFiles` SKIP_DIRS 补 `benchmarks`(与 extract 口径一致,本仓库自身不再误标 C);②`#if 0` 死代码块预处理剥离(真实仓库不平衡花括号不再腐蚀函数体计数);③顶层 `#` 行只跳自身不再吞相邻函数(openssl 14,394→15,896 函数,黄金函数恢复率升至 89–100%);④`buildCallSequences` 同文件定义优先绑定(跨文件同名 static 不再 last-wins 错绑,入口判定文件化;Python 盲测 v1.2 复测零漂移——报告与基线逐字节一致仅时间戳不同);⑤提取器取消调用去重(状态机重复调用有语义,双 close/重复 logout 可检出,与 TS/Python 提取器一致);⑥删死代码 `isCProject`;⑦混合 TS+C 项目回归测试。应用级 C 金标扩至 v2(11 clean × 11 违规 + helper 中介风格 + 逐命名空间分解):**TP 11/11 FP 1 FN 0 → P=91.7% / R=100% / F1=95.7%**(唯一 FP 为 do_logout 跨函数窗口边界)。已知系统性风险文档化:C 前缀包装器(`ngx_*` 等)撞词段匹配(nginx 3 FP),缓解方案(前缀剥离/连续词段)留待下一轮并强制盲测复跑
13
56
  - **限制如实记录**:函数指针分发静态不可见(L3 结论不变)、宏/K&R/C++ 不解析、无数据流/指针分析(L4 无计划);提取器遍历跳过 vendored `benchmarks/`;`docs/c-language-status.md` 已更新(新路线小节 + 基准结果 + Decision record)
package/README.md CHANGED
@@ -103,7 +103,7 @@ Progmune is honest about what it can and cannot verify.
103
103
  |----------|--------|----------|
104
104
  | **TypeScript / JavaScript** | ✅ Production | Blind benchmark: **recall 98.5% / precision 100%** (795 gold findings, 100 projects) |
105
105
  | **Python** | ✅ Production | Blind benchmark: **recall 100% / precision 100%** (729 gold findings, 90 projects); real-world validation: PyGoat (OWASP vulnerable-by-design Django app) **67 TP / 0 FP, 100% labeled precision**; three well-written apps (django/fastapi realworld, django-unicorn) with 0 false-positive true findings |
106
- | **C** | ⚠️ Research-only | 3.7.4: IR extraction merged via the language registry app-level protocol lifecycles (auth/db/file/payment) verifiable through the SSG state machine (app-level gold v2: **P=91.7% / R=100% / F1=95.7%**). TLS-level coverage still absent (old regex-route F1=16.5% is the historical baseline); L3/L4 conclusions unchanged. See [C Language Status](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/c-language-status.md). |
106
+ | **C** | Annotation-driven (Beta) | IR extraction via the registry + SSG state machine; **annotate ~2-3 protocol primitives per protocol to get trusted verification** (gold 5/5 on real modules: redis ACL / libssh client / libssh server / libssh callback-dispatch / uftpd transfer-auth — all 0 FP with precise violation localization; app-level gold v2: **P=91.7% / R=100% / F1=95.7%**). Unannotated auto-detection stays out of scope (0 TP on real corpus — positioning decision recorded in the C Language Status doc); TLS-level coverage still absent. See [C Language Status](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/c-language-status.md). |
107
107
  | **Go, Java** | ❌ None | Planned |
108
108
 
109
109
  **Multi-language IR (registry-based).** TypeScript (ts-morph) and Python (AST) extractors are registered entries in one registry (`src/extract-project-ir.ts`); `extractProjectIR` merges every detected language into a single function IR shared by the agent loop, `execute()`, and the MCP server — the agent composes function-protocol chains across both languages. Adding a language (Go, Java, …) = register one extractor entry; callers don't change.
@@ -152,9 +152,9 @@ Public, reproducible precision data. All numbers measured against gold-annotated
152
152
 
153
153
  → [Real-world validation report](https://github.com/shenlian19831109/progmune-runtime/blob/main/blind-benchmark/REALWORLD_APP_V1.md) · [Benchmark baseline](https://github.com/shenlian19831109/progmune-runtime/blob/main/blind-benchmark/BASELINE_v6.md)
154
154
 
155
- ### C (IR extraction + app-level protocol verification — research status)
155
+ ### C (IR extraction + annotation-driven protocol verification — Beta)
156
156
 
157
- Since 3.7.4, C has an IR extractor in the multi-language registry: C projects flow into IR-first sequence validation + the SSG state machine. App-level protocol lifecycles (auth/db/file/payment) are verifiable on C — app-level gold v2: **11/11 violations caught (Recall 100%), 1 FP, F1=95.7%**; extraction scales to 10k+ functions per repo in seconds with 89–100% gold-function recovery (curl/libssh/nginx/openssl/nghttp2/redis). Two boundaries remain: the old regex-route gold benchmark F1=16.5% measured **TLS-level misuse** (SSG has no TLS state machines — that gap is unchanged), and L3/L4 conclusions stand (function-pointer dispatch is statically invisible; no pointer/CFG analysis planned). See [C Language Status](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/c-language-status.md) for the full picture and reasoning.
157
+ Since 3.7.4, C has an IR extractor in the multi-language registry: C projects flow into IR-first sequence validation + the SSG state machine. App-level protocol lifecycles (auth/db/file/payment) are verifiable on C — app-level gold v2: **11/11 violations caught (Recall 100%), 1 FP, F1=95.7%**. Annotation-driven verification is the production shape (Beta as of 3.7.6): gold 5/5 on real modules (redis ACL, libssh client/server/callback-dispatch, uftpd transfer-auth) + one independent adoption case (uftpd) — all 0 FP with precise violation localization, at a steady cost of ~2-3 annotations per protocol. Unannotated auto-detection is out of scope (0 TP on real corpus). Extraction covers large real repos in seconds (openssl 15.5k / redis 5.7k / curl 4.2k functions, gold-function recovery 97–100%). Two boundaries remain: the old regex-route gold benchmark F1=16.5% measured **TLS-level misuse** (SSG has no TLS state machines — that gap is unchanged), and L3/L4 conclusions stand (function-pointer dispatch is statically invisible; no pointer/CFG analysis planned). See [C Language Status](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/c-language-status.md) for the full picture and reasoning.
158
158
 
159
159
  ### P0-P3 Rule Injection (2026-08)
160
160
 
package/README.zh-CN.md CHANGED
@@ -103,7 +103,7 @@ Progmune 对能验证什么、不能验证什么保持诚实。
103
103
  |------|------|------|
104
104
  | **TypeScript / JavaScript** | ✅ 生产 | 盲测基准:**召回 98.5% / 精确率 100%**(795 条 gold finding,100 个项目) |
105
105
  | **Python** | ✅ 生产 | 盲测基准:**召回 100% / 精确率 100%**(729 条 gold finding,90 个项目);真实应用验证:PyGoat(OWASP 故意脆弱 Django 应用)**67 TP / 0 FP,标记精确率 100%**;三个良构应用(django/fastapi realworld、django-unicorn)0 误报真阳性 |
106
- | **C** | ⚠️ 仅研究 | 3.7.4:IR 提取接入多语言注册表——应用级协议生命周期(认证/数据库/文件/支付)经 SSG 状态机可验证(应用级金标 v2:**P=91.7% / R=100% / F1=95.7%**)。TLS 级覆盖仍无(旧正则口径 F1=16.5% 为历史基线);L3/L4 结论不变。见 [C 语言状态](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/c-language-status.md)。 |
106
+ | **C** | 注解驱动(Beta) | IR 提取接入注册表 + SSG 状态机;**每协议标注 ~2-3 个原语即获得可信验证**(真实模块金标 5/5:redis ACL / libssh 客户端 / libssh 服务端 / libssh 回调分发 / uftpd 传送授权——全部 0 误报 + 违规精确定位;应用级金标 v2:**P=91.7% / R=100% / F1=95.7%**)。未注解自动检测不在范围(真实语料 0 TP——定位决议见 C 语言状态文档);TLS 级覆盖仍无。见 [C 语言状态](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/c-language-status.md)。 |
107
107
  | **Go, Java** | ❌ 无 | 规划中 |
108
108
 
109
109
  **多语言 IR(注册表式)。** TypeScript(ts-morph)与 Python(AST)提取器是同一注册表(`src/extract-project-ir.ts`)中的注册项;`extractProjectIR` 把检测到的所有语言合并为一份函数 IR,由 agent loop、`execute()` 与 MCP server 共享——agent 可在两种语言上编排函数协议链。新增语言(Go、Java……)= 注册一条提取器,调用方零改动。
@@ -152,9 +152,9 @@ Progmune 对能验证什么、不能验证什么保持诚实。
152
152
 
153
153
  → [真实验证报告](https://github.com/shenlian19831109/progmune-runtime/blob/main/blind-benchmark/REALWORLD_APP_V1.md) · [基准基线](https://github.com/shenlian19831109/progmune-runtime/blob/main/blind-benchmark/BASELINE_v6.md)
154
154
 
155
- ### C(IR 提取 + 应用级协议验证——研究状态)
155
+ ### C(IR 提取 + 注解驱动协议验证——Beta)
156
156
 
157
- 3.7.4 起 C 拥有多语言注册表中的 IR 提取器:C 项目进入 IR-first 序列验证 + SSG 状态机。应用级协议生命周期(认证/数据库/文件/支付)在 C 上可验证——应用级金标 v2:**11/11 违规全检出(召回 100%)、1 误报、F1=95.7%**;提取规模化至每仓库 1 万+ 函数、秒级完成、黄金函数恢复率 89–100%(curl/libssh/nginx/openssl/nghttp2/redis)。两条边界不变:旧正则口径黄金基准 F1=16.5% 测的是 **TLS 级误用**(SSG 无 TLS 状态机,该口径不变);L3/L4 结论维持(函数指针分发静态不可见;无指针/CFG 分析计划)。详见 [C 语言状态](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/c-language-status.md)。
157
+ 3.7.4 起 C 拥有多语言注册表中的 IR 提取器:C 项目进入 IR-first 序列验证 + SSG 状态机。应用级协议生命周期(认证/数据库/文件/支付)在 C 上可验证——应用级金标 v2:**11/11 违规全检出(召回 100%)、1 误报、F1=95.7%**。注解驱动是 C 的生产形态(3.7.6 Beta):真实模块金标 5/5(redis ACL、libssh 客户端/服务端/回调分发、uftpd 传送授权)+ 1 个独立采纳案例(uftpd)——全部 0 误报 + 违规精确定位,标注成本稳定在每协议 ~2-3 条。未注解自动检测不在范围(真实语料 0 TP)。提取覆盖大型真实仓库(openssl 15.5k / redis 5.7k / curl 4.2k 函数,秒级,黄金函数恢复率 97–100%)。两条边界不变:旧正则口径黄金基准 F1=16.5% 测的是 **TLS 级误用**(SSG 无 TLS 状态机,该口径不变);L3/L4 结论维持(函数指针分发静态不可见;无指针/CFG 分析计划)。详见 [C 语言状态](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/c-language-status.md)。
158
158
 
159
159
  ### P0-P3 规则注入(2026-08)
160
160
 
package/c-aliases.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "description": "C 库边界别名共享表——注解驱动定位的孵化器燃料(跨项目迁移)",
3
+ "entries": [
4
+ {
5
+ "call": "ssh_userauth_password",
6
+ "rule": "verify_password",
7
+ "status": "confirmed",
8
+ "proposedBy": "demo-real-c-libssh",
9
+ "proposedAt": "2026-08-27",
10
+ "confirmedAt": "2026-08-27"
11
+ }
12
+ ]
13
+ }
@@ -13,10 +13,17 @@
13
13
  * 不做数据流/指针/分支分析;跨文件依赖 IR 的 calls[] 图。
14
14
  */
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.MAX_SEQUENCE_CALLS = void 0;
16
17
  exports.isProjectFn = isProjectFn;
17
18
  exports.collectProjectFunctionNames = collectProjectFunctionNames;
18
19
  exports.buildCallSequences = buildCallSequences;
19
20
  const MAX_DEPTH = 4;
21
+ /**
22
+ * 每序列调用预算上限。真实语料实测:Python 盲测语料 350 序列最大 23、
23
+ * TS 自身 IR 469 序列最大 824——2,000 对现有语料零影响;C 巨型函数
24
+ * (openssl 单序列可达 1M+ 调用)在预算处截断。
25
+ */
26
+ exports.MAX_SEQUENCE_CALLS = 2000;
20
27
  /** 项目函数判定:有真实文件且非外部导入条目(external 条目无函数体可内联)。 */
21
28
  function isProjectFn(f) {
22
29
  return !f.external && !!f.file && f.file !== "(external)";
@@ -53,7 +60,7 @@ function collectProjectFunctionNames(ir) {
53
60
  * (其调用名保留给规则匹配),不内联其函数体——否则 create_session 等
54
61
  * 规则函数的平凡函数体会把调用名"吞掉"
55
62
  */
56
- function buildCallSequences(ir, keepNames) {
63
+ function buildCallSequences(ir, keepNames, maxCalls = exports.MAX_SEQUENCE_CALLS) {
57
64
  // 全局按名回退 + 同文件优先映射:C 中跨文件同名 static 函数极常见
58
65
  // (每个 .c 都有 static cleanup/helper),名字级 Map 会让 last-wins 的
59
66
  // 定义绑定到错误的调用方。同翻译单元(文件)定义优先解析——
@@ -88,12 +95,23 @@ function buildCallSequences(ir, keepNames) {
88
95
  calledBy.add(fnKey(resolved.file, resolved.name));
89
96
  }
90
97
  }
98
+ /** 调用预算:预算制展开——入口自身调用按序优先,预算耗尽即停(截断),
99
+ * 不在事后截断百万级序列(内存/时间双浪费) */
100
+ const budget = { left: maxCalls, truncated: false };
91
101
  /** 展开函数体内的调用(入口序列 = 函数体调用,不含函数自己的名字) */
92
102
  const expandBody = (fn, depth, visiting) => {
103
+ if (budget.left <= 0) {
104
+ budget.truncated = true;
105
+ return [];
106
+ }
93
107
  const out = [];
94
108
  for (const c of fn.calls || []) {
95
109
  if (typeof c !== "string" || c.startsWith("__progmune_"))
96
110
  continue;
111
+ if (budget.left <= 0) {
112
+ budget.truncated = true;
113
+ return out;
114
+ }
97
115
  out.push(...expandCall(c, depth, visiting, fn.file));
98
116
  }
99
117
  return out;
@@ -102,14 +120,19 @@ function buildCallSequences(ir, keepNames) {
102
120
  if (depth > MAX_DEPTH || visiting.has(name))
103
121
  return [];
104
122
  const fn = resolveCall(fromFile, name);
105
- // 外部调用或规则函数:调用名保留给匹配层,不内联
106
- if (!fn || (keepNames && keepNames.has(name)))
123
+ // 外部调用或规则函数:调用名保留给匹配层,不内联(记 1 个调用)
124
+ if (!fn || (keepNames && keepNames.has(name))) {
125
+ budget.left--;
107
126
  return [name];
127
+ }
108
128
  // 叶子函数(函数体只调外部原语)是协议原语或叶子 helper:
109
129
  // 保留名字,不内联——否则 S5 改名协议函数的平凡函数体会吞掉调用名
110
130
  const hasProjectCalls = (fn.calls || []).some((c) => resolveCall(fn.file, c));
111
- if (!hasProjectCalls)
131
+ if (!hasProjectCalls) {
132
+ budget.left--;
112
133
  return [name];
134
+ }
135
+ // 内联:函数体自身的调用在递归内各自记账
113
136
  visiting.add(name);
114
137
  const out = expandBody(fn, depth + 1, visiting);
115
138
  visiting.delete(name);
@@ -123,10 +146,18 @@ function buildCallSequences(ir, keepNames) {
123
146
  continue; // 非入口:片段并入调用方
124
147
  if (keepNames && keepNames.has(f.name))
125
148
  continue; // 协议原语不是入口:只在调用链内验证
149
+ budget.left = maxCalls;
150
+ budget.truncated = false;
126
151
  const calls = expandBody(f, 0, new Set());
127
152
  if (calls.length === 0)
128
153
  continue;
129
- sequences.push({ calls, file: f.file, function: f.name });
154
+ sequences.push({
155
+ calls,
156
+ file: f.file,
157
+ function: f.name,
158
+ truncated: budget.truncated || undefined,
159
+ directCalls: f.calls || [],
160
+ });
130
161
  }
131
162
  return sequences;
132
163
  }
@@ -82,6 +82,46 @@ function fn(name, file, calls) {
82
82
  (0, vitest_1.expect)(seqs[0].calls).toEqual(["verify_password"]);
83
83
  });
84
84
  });
85
+ (0, vitest_1.describe)("buildCallSequences(调用预算 / 截断,兆级序列防护)", () => {
86
+ (0, vitest_1.it)("超预算截断:序列长度 = maxCalls,truncated 标记", () => {
87
+ const ext = Array.from({ length: 15 }, (_, i) => `ext_${i}`);
88
+ const ir = [fn("entry", "a.c", ext)];
89
+ const seqs = (0, call_sequence_1.buildCallSequences)(ir, undefined, 10);
90
+ (0, vitest_1.expect)(seqs).toHaveLength(1);
91
+ (0, vitest_1.expect)(seqs[0].calls).toHaveLength(10);
92
+ (0, vitest_1.expect)(seqs[0].calls).toEqual(ext.slice(0, 10)); // 入口自身调用按序优先
93
+ (0, vitest_1.expect)(seqs[0].truncated).toBe(true);
94
+ });
95
+ (0, vitest_1.it)("预算内序列:无 truncated 标记(现有语料零影响)", () => {
96
+ const ir = [fn("entry", "a.c", ["g", "h"])];
97
+ const seqs = (0, call_sequence_1.buildCallSequences)(ir, undefined, 2000);
98
+ (0, vitest_1.expect)(seqs).toHaveLength(1);
99
+ (0, vitest_1.expect)(seqs[0].calls).toEqual(["g", "h"]);
100
+ (0, vitest_1.expect)(seqs[0].truncated).toBeUndefined();
101
+ });
102
+ (0, vitest_1.it)("内联超预算:已发出的调用保留,入口后续调用被截(召回边界语义)", () => {
103
+ const bigBody = Array.from({ length: 20 }, (_, i) => `big_call_${i}`);
104
+ const ir = [
105
+ fn("helper", "a.c", ["ext"]), // 项目函数:big 因调它而可内联(叶子规则)
106
+ fn("big", "a.c", ["helper", ...bigBody]),
107
+ fn("entry", "a.c", ["big", "tail_call"]),
108
+ ];
109
+ const seqs = (0, call_sequence_1.buildCallSequences)(ir, undefined, 5);
110
+ (0, vitest_1.expect)(seqs).toHaveLength(1);
111
+ (0, vitest_1.expect)(seqs[0].calls).toEqual(["helper", "big_call_0", "big_call_1", "big_call_2", "big_call_3"]);
112
+ (0, vitest_1.expect)(seqs[0].truncated).toBe(true);
113
+ });
114
+ (0, vitest_1.it)("多入口各自独立预算", () => {
115
+ const ext = Array.from({ length: 8 }, (_, i) => `ext_${i}`);
116
+ const ir = [fn("e1", "a.c", ext), fn("e2", "b.c", ext)];
117
+ const seqs = (0, call_sequence_1.buildCallSequences)(ir, undefined, 5);
118
+ (0, vitest_1.expect)(seqs).toHaveLength(2);
119
+ for (const s of seqs) {
120
+ (0, vitest_1.expect)(s.calls).toHaveLength(5);
121
+ (0, vitest_1.expect)(s.truncated).toBe(true);
122
+ }
123
+ });
124
+ });
85
125
  (0, vitest_1.describe)("collectProjectFunctionNames", () => {
86
126
  (0, vitest_1.it)("全名/裸名/小写三形态收录", () => {
87
127
  const ir = [fn("FlowService.svc_x", "a.ts", [])];
@@ -64,6 +64,22 @@ const SKIP_DIRS = new Set([
64
64
  "node_modules", "dist", "build", ".git", ".progmune_corpus",
65
65
  "__pycache__", "venv", ".venv", "benchmarks",
66
66
  ]);
67
+ /**
68
+ * 非生产表面目录(与 tools/extract_ir.py 的 Python 先例对齐:
69
+ * "Skip test files — tests are not production surface for a security
70
+ * scanner"):安全扫描只看随产品发布的生产代码路径。真实 C 仓库验证
71
+ * (libssh/redis/nginx,scan-real-c.ts)中 65/79 条误报来自 tests/
72
+ * examples/ 与 vendored deps/(jemalloc)。
73
+ */
74
+ const NON_SURFACE_DIRS = new Set([
75
+ "tests", "test", "examples", "docs", "docs_src", "scripts",
76
+ "deps", "vendor", "third_party",
77
+ ]);
78
+ /** 测试文件名模式(test_*.c / *_test.c / *_test.h,Python 先例同款) */
79
+ function isTestFilename(name) {
80
+ const base = name.replace(/\.[^.]+$/, "");
81
+ return base.startsWith("test_") || base.endsWith("_test");
82
+ }
67
83
  /** Copied from sequence-extractor.ts:107-111 (kept local — decoupled benchmark path must not change). */
68
84
  const C_KEYWORDS = new Set([
69
85
  "if", "for", "while", "switch", "return", "sizeof", "typeof",
@@ -384,8 +400,12 @@ function stripAttributes(s) {
384
400
  * type-name candidates, and derive the return type from the buffer text
385
401
  * BEFORE the chosen candidate. `[^;]*?` keeps the params scan bounded.
386
402
  * Groups: 1 = name, 2 = params.
403
+ * Lookbehind anchor: the old `(?:^|\s)` silently dropped every single-line
404
+ * pointer-return definition (`char *foo(`, `SSL *foo(` — name preceded by
405
+ * `*`/`&`, not whitespace). `(?<![a-zA-Z0-9_])` allows those while still
406
+ * rejecting identifiers glued to the name.
387
407
  */
388
- const FUNC_CAND_RE = /(?:^|\s)([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^;]*?)\)\s*\{/g;
408
+ const FUNC_CAND_RE = /(?<![a-zA-Z0-9_])([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^;]*?)\)\s*\{/g;
389
409
  /** struct/union/enum definition bodies must be skipped, not parsed as functions. */
390
410
  const STRUCT_DECL_RE = /^(?:typedef\s+)?(?:struct|union|enum)\b/;
391
411
  /** Call-name + goto extraction filters. */
@@ -675,10 +695,10 @@ function collectCFiles(projectRoot) {
675
695
  for (const e of entries) {
676
696
  const full = path.join(dir, e.name);
677
697
  if (e.isDirectory()) {
678
- if (!SKIP_DIRS.has(e.name) && !e.name.startsWith("."))
698
+ if (!SKIP_DIRS.has(e.name) && !NON_SURFACE_DIRS.has(e.name) && !e.name.startsWith("."))
679
699
  stack.push(full);
680
700
  }
681
- else if (C_EXTENSIONS.has(path.extname(e.name))) {
701
+ else if (C_EXTENSIONS.has(path.extname(e.name)) && !isTestFilename(e.name)) {
682
702
  out.push(full);
683
703
  }
684
704
  }
@@ -359,6 +359,24 @@ int g(void) { return 1; }
359
359
  (0, vitest_1.expect)(Date.now() - t0).toBeLessThan(2000);
360
360
  (0, vitest_1.expect)(ir.map((x) => x.name)).toEqual(["f", "g"]);
361
361
  });
362
+ (0, vitest_1.it)("非生产表面目录被跳过:tests/examples/deps 与测试文件名(Python 先例同款)", () => {
363
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-surface-"));
364
+ try {
365
+ fs.writeFileSync(path.join(dir, "main.c"), "void prod(void) {}\n");
366
+ fs.mkdirSync(path.join(dir, "tests"), { recursive: true });
367
+ fs.writeFileSync(path.join(dir, "tests", "torture_x.c"), "void in_tests(void) {}\n");
368
+ fs.mkdirSync(path.join(dir, "examples"), { recursive: true });
369
+ fs.writeFileSync(path.join(dir, "examples", "demo.c"), "void in_examples(void) {}\n");
370
+ fs.mkdirSync(path.join(dir, "deps", "vendor_lib"), { recursive: true });
371
+ fs.writeFileSync(path.join(dir, "deps", "vendor_lib", "lib.c"), "void in_deps(void) {}\n");
372
+ fs.writeFileSync(path.join(dir, "helper_test.c"), "void testfile(void) {}\n");
373
+ const ir = (0, extract_ir_c_1.extractIRC)(dir);
374
+ (0, vitest_1.expect)(ir.map((f) => f.name)).toEqual(["prod"]);
375
+ }
376
+ finally {
377
+ fs.rmSync(dir, { recursive: true, force: true });
378
+ }
379
+ });
362
380
  (0, vitest_1.it)("#if 0 死代码块被剥离:体内不平衡花括号不腐蚀计数", () => {
363
381
  const ir = parse(`
364
382
  void f(void) {
@@ -398,3 +416,23 @@ void after(void) { h(); }
398
416
  (0, vitest_1.expect)(fn(ir, "maybe_live").calls).toEqual(["g"]);
399
417
  });
400
418
  });
419
+ (0, vitest_1.describe)("extract-ir-c pointer-return regression", () => {
420
+ (0, vitest_1.it)("单行指针返回函数必须被提取(char */SSL */const char */FILE * 等)", () => {
421
+ const src = [
422
+ "int a(void) { return 1; }",
423
+ "char *b(void) { return 0; }",
424
+ "SSL *c(SSL_CTX *ctx) { return 0; }",
425
+ "const char *d(int x) { return 0; }",
426
+ "static FILE *f(void) { return 0; }",
427
+ "int (*handler)(int) { return 0; }",
428
+ ].join("\n");
429
+ const fns = (0, extract_ir_c_1.parseCSource)(src, "ptr.c", "/");
430
+ const names = fns.map((f) => f.name);
431
+ (0, vitest_1.expect)(names).toEqual(vitest_1.expect.arrayContaining(["b", "c", "d", "f"]));
432
+ // 函数指针变量不是函数定义
433
+ (0, vitest_1.expect)(names).not.toContain("handler");
434
+ // 返回类型保留指针
435
+ const c = fns.find((f) => f.name === "c");
436
+ (0, vitest_1.expect)(c?.returnType).toBe("SSL *");
437
+ });
438
+ });
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.4";
23
+ exports.RUNTIME_VERSION = "3.7.6";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
@@ -163,6 +163,13 @@ function findFixPathStatic(rules, namespace, current, targetPreStates) {
163
163
  nsFuncs.push({ name: fn, rule });
164
164
  }
165
165
  }
166
+ // 项目注解原语优先展开(修复路径输出真实函数名而非通用规则名);
167
+ // stable sort:无 displayName 的规则(内置/TS/Python 惯例)相对顺序不变
168
+ nsFuncs.sort((a, b) => {
169
+ const pa = a.rule.displayName ? 0 : 1;
170
+ const pb = b.rule.displayName ? 0 : 1;
171
+ return pa - pb;
172
+ });
166
173
  // BFS
167
174
  const startKey = [...new Set(current)].sort().join(",");
168
175
  const visited = new Set();
@@ -63,7 +63,20 @@ const protocol_domain_validator_1 = require("./protocol-domain-validator");
63
63
  const call_graph_propagator_1 = require("./call-graph-propagator");
64
64
  const ssg_bridge_1 = require("./ssg-bridge");
65
65
  const call_sequence_1 = require("../call-sequence");
66
+ const extract_ir_1 = require("../extract-ir");
67
+ const extract_ir_python_1 = require("../extract-ir-python");
68
+ const extract_ir_c_1 = require("../extract-ir-c");
66
69
  // ── Main Entry Point ──
70
+ /**
71
+ * Trust 决策主入口:收集 → 归一化 → 评分 → 决策 → 组装。
72
+ *
73
+ * IR 写盘语义(勿改):项目缺少 ir.json 时,引擎按 ctx.language 自动提取
74
+ * 并落盘(TS/JS → extractIR 裸数组;Python → extractIRPython 裸数组;
75
+ * C → extractIRC 合并形态 { typeMap, functions })——注解合并(P4.5)依赖
76
+ * ir.json,调用方【无需】手动 extractProjectIR/写盘(曾是对 C 注解静默
77
+ * 失效的文档/API 陷阱,回归测试见 tests/trust/engine.test.ts「DSH 陷阱」)。
78
+ * 若项目已有 ir.json,以现有文件为准(不覆盖)。
79
+ */
67
80
  async function evaluateTrust(ctx) {
68
81
  const engineVersion = "trust-runtime-v1.0.0";
69
82
  const timestamp = new Date().toISOString();
@@ -709,41 +722,44 @@ async function collectProtocolViolations(ctx, callGraph) {
709
722
  // 序列必须来自「函数体内真实调用」而非「文件内函数声明名单」——
710
723
  // 声明顺序 ≠ 执行顺序:正则扫描会把声明当调用(auth.ts 误报),
711
724
  // 也会因调用数不足阈值漏掉单调用违规文件(bad_flow 漏报)。
712
- if (!ctx.language || ctx.language === "typescript" || ctx.language === "javascript") {
713
- try {
714
- const fs = require("fs");
715
- if (!fs.existsSync(path.join(ctx.projectPath, "ir.json"))) {
716
- const { extractIR } = require("../extract-ir");
717
- const ir = extractIR(ctx.projectPath);
718
- fs.writeFileSync(path.join(ctx.projectPath, "ir.json"), JSON.stringify(ir, null, 2));
725
+ // 注解合并(P4.5)依赖 ir.json 写盘:此前仅 TS/JS 自动提取,C/Python 项目
726
+ // 直接调 evaluateTrust 时注解静默失效(DSH 复测发现的文档/API 陷阱)——
727
+ // 按语言分派提取器兜底写盘;TS/JS 路径保持原样(裸数组形态,零变化)。
728
+ {
729
+ const lang = ctx.language || "typescript";
730
+ // 静态导入(vitest 环境下 lazy require 的 CJS 互操作不可靠——
731
+ // TS 路径从未触发过该分支:仓库根遗留 ir.json 短路了 existsSync 检查)
732
+ const autoExtractor = {
733
+ typescript: () => (0, extract_ir_1.extractIR)(ctx.projectPath),
734
+ javascript: () => (0, extract_ir_1.extractIR)(ctx.projectPath),
735
+ python: () => (0, extract_ir_python_1.extractIRPython)(ctx.projectPath),
736
+ c: () => (0, extract_ir_c_1.extractIRC)(ctx.projectPath),
737
+ };
738
+ const extractFn = autoExtractor[lang];
739
+ if (extractFn) {
740
+ try {
741
+ const fs = require("fs");
742
+ if (!fs.existsSync(path.join(ctx.projectPath, "ir.json"))) {
743
+ const ir = extractFn();
744
+ // C 走合并形态(与 execute/MCP 写盘一致);TS/Python 保持裸数组形态
745
+ const payload = lang === "c" ? { typeMap: {}, functions: ir } : ir;
746
+ fs.writeFileSync(path.join(ctx.projectPath, "ir.json"), JSON.stringify(payload, null, 2));
747
+ }
719
748
  }
749
+ catch { /* best-effort — 回退正则扫描 */ }
720
750
  }
721
- catch { /* best-effort — 回退正则扫描 */ }
722
751
  }
723
752
  // ── SSG State Machine: load protocol rules once ──
724
753
  protocolRulesData = (0, ssg_bridge_1.loadProtocolRules)(ctx.projectPath);
725
- // ── Phase 1-5 Semantic Pipeline ──
726
- // 规则名集合作为展开的保留单元:规则函数不内联(调用名保留给匹配层)
727
- const callSequences = extractCallSequencesFromProject(ctx.projectPath, ctx.language, protocolRulesData ? new Set(protocolRulesData.rules.keys()) : undefined);
728
- const flaggedCount = { value: 0 };
729
- const cleanCount = { value: 0 };
730
- // ── P4.6.1: 词段匹配门控的项目函数集合(best-effort,与注解合并共用 ir.json) ──
731
- // 词段匹配只对项目函数适用(改名协议原语);外部库调用走 alias/关键词桥接。
732
- let projectFunctions;
733
- try {
734
- const fs = require("fs");
735
- const irPath = path.join(ctx.projectPath, "ir.json");
736
- if (fs.existsSync(irPath)) {
737
- const ir = JSON.parse(fs.readFileSync(irPath, "utf-8"));
738
- const functions = Array.isArray(ir) ? ir : (ir.functions || []);
739
- projectFunctions = (0, call_sequence_1.collectProjectFunctionNames)(functions);
740
- }
741
- }
742
- catch { /* best-effort */ }
743
754
  // ── P4.5: 合并项目 IR 注解协议(IR 优先,缺 namespace 继承内置 JSON) ──
744
755
  // 内置 protocols.json 的规则是通用弱约束(如 generate_jwt pre=[]),
745
756
  // 项目文件里的 @protocol 注解才是项目真实协议(如 pre=[PASSWORD_VERIFIED])。
746
757
  // planner 已用此合并语义,trust 引擎需对齐,否则项目级前置约束不生效。
758
+ // 合并必须在序列构建【之前】:规则名是展开的保留单元(不内联),若合并
759
+ // 晚于 extractCallSequencesFromProject,注解原语(有函数体、调项目函数的)
760
+ // 会被内联掉,其 post 状态永不生效——真实 redis ACL 演示暴露(
761
+ // checkPasswordBasedAuth 被内联 → AUTHENTICATED 未建立 → good 流误报)。
762
+ // 与盲测 harness(scan-protocol-python)先合并后建序列的语义对齐。
747
763
  if (protocolRulesData) {
748
764
  try {
749
765
  const fs = require("fs");
@@ -761,12 +777,40 @@ async function collectProtocolViolations(ctx, callGraph) {
761
777
  if (existing?.namespace && !protocol.namespace) {
762
778
  protocol.namespace = existing.namespace;
763
779
  }
780
+ // 修复路径渲染真实函数名(fixPath 输出项目原语而非通用规则名)
781
+ protocol.displayName = String(f.name);
764
782
  protocolRulesData.rules.set(String(f.name), protocol);
783
+ // CamelCase 真实命名(C 代码普遍,如 ACLCheckAllPerm)注册的规则
784
+ // 原样无法被任何匹配策略触达(normalize 只作用于调用名;词段匹配
785
+ // 要求 ≥2 个下划线词段)——同步注册规范化形态使注解原语可被按名命中。
786
+ // 加性改动:snake_case 注解(TS/Python 惯例)normalized === 原名,无变化。
787
+ const normalized = (0, ssg_bridge_1.normalizeName)(String(f.name));
788
+ if (normalized !== String(f.name)) {
789
+ protocolRulesData.rules.set(normalized, protocol);
790
+ }
765
791
  }
766
792
  }
767
793
  }
768
794
  catch { /* best-effort */ }
769
795
  }
796
+ // ── Phase 1-5 Semantic Pipeline ──
797
+ // 规则名集合作为展开的保留单元:规则函数不内联(调用名保留给匹配层)
798
+ const callSequences = extractCallSequencesFromProject(ctx.projectPath, ctx.language, protocolRulesData ? new Set(protocolRulesData.rules.keys()) : undefined);
799
+ const flaggedCount = { value: 0 };
800
+ const cleanCount = { value: 0 };
801
+ // ── P4.6.1: 词段匹配门控的项目函数集合(best-effort,与注解合并共用 ir.json) ──
802
+ // 词段匹配只对项目函数适用(改名协议原语);外部库调用走 alias/关键词桥接。
803
+ let projectFunctions;
804
+ try {
805
+ const fs = require("fs");
806
+ const irPath = path.join(ctx.projectPath, "ir.json");
807
+ if (fs.existsSync(irPath)) {
808
+ const ir = JSON.parse(fs.readFileSync(irPath, "utf-8"));
809
+ const functions = Array.isArray(ir) ? ir : (ir.functions || []);
810
+ projectFunctions = (0, call_sequence_1.collectProjectFunctionNames)(functions);
811
+ }
812
+ }
813
+ catch { /* best-effort */ }
770
814
  for (const seq of callSequences) {
771
815
  try {
772
816
  const semantic = await (0, api_semantic_mapper_1.mapSequenceToSemanticWithLLM)(seq.calls);
@@ -835,7 +879,7 @@ async function collectProtocolViolations(ctx, callGraph) {
835
879
  // A sequence may be a valid protocol operation but still violate
836
880
  // specific security requirements (e.g., TLS without cert verify)
837
881
  try {
838
- const specificViolations = (0, protocol_domain_validator_1.checkSpecificViolations)(semantic, seq.file ? path.join(ctx.projectPath, seq.file) : undefined);
882
+ const specificViolations = (0, protocol_domain_validator_1.checkSpecificViolations)(semantic, seq.file ? path.join(ctx.projectPath, seq.file) : undefined, ctx.language);
839
883
  for (const sv of specificViolations) {
840
884
  violations.push({
841
885
  severity: "medium", // Phase 2: per-function window has inherent limitations
@@ -858,7 +902,7 @@ async function collectProtocolViolations(ctx, callGraph) {
858
902
  // to bridge real API names to abstract protocol function names.
859
903
  if (protocolRulesData) {
860
904
  try {
861
- const ssgResult = (0, ssg_bridge_1.validateSequenceWithSSG)(semantic.steps, protocolRulesData.rules, protocolRulesData.namespaceInitialStates, seq.file, protocolRulesData.aliasIndex, protocolRulesData.wildcardAliases, projectFunctions);
905
+ const ssgResult = (0, ssg_bridge_1.validateSequenceWithSSG)(semantic.steps, protocolRulesData.rules, protocolRulesData.namespaceInitialStates, seq.file, protocolRulesData.aliasIndex, protocolRulesData.wildcardAliases, projectFunctions, seq.directCalls ? new Set(seq.directCalls) : undefined);
862
906
  ssgResults.push(ssgResult);
863
907
  ssgTotalCalls += ssgResult.stats.totalCalls;
864
908
  ssgMatchedCalls += ssgResult.stats.matchedCalls;
@@ -284,6 +284,10 @@ exports.SPECIFIC_VIOLATION_CHECKS = [
284
284
  ruleId: "PLAINTEXT_AUTH_WITHOUT_TLS",
285
285
  description: "Authentication credentials transmitted without TLS protection. " +
286
286
  "SASL PLAIN, LOGIN, or credential data must be sent over an encrypted channel.",
287
+ // C 排除(2026-08-28,真实语料证据):libssh 演示 1 FP(SSH 明文协议被
288
+ // Web/TLS 语义误映射)+ uftpd 采纳案例 2 FP(handle_USER/main)——FTP/SSH
289
+ // 应用层本就明文,传输加密在协议自身/进程外,本规则的 Web 语义对 C 无 TP。
290
+ languages: ["typescript", "javascript", "python"],
287
291
  check: (steps) => {
288
292
  // Auth data being prepared (credentials + mechanism)
289
293
  const hasAuthCred = steps.some((s) => s.domain === "auth_cred");
@@ -649,10 +653,16 @@ exports.SPECIFIC_VIOLATION_CHECKS = [
649
653
  /**
650
654
  * Run specific violation checks against a semantic sequence.
651
655
  * Returns violations found (empty for Phase 1 MVP).
656
+ *
657
+ * @param language — project language ("c" | "python" | "typescript" | …);
658
+ * checks with a `languages` allowlist are skipped for other languages.
652
659
  */
653
- function checkSpecificViolations(seq, _filePath) {
660
+ function checkSpecificViolations(seq, _filePath, language) {
654
661
  const violations = [];
655
662
  for (const check of exports.SPECIFIC_VIOLATION_CHECKS) {
663
+ if (check.languages && language && !check.languages.includes(language)) {
664
+ continue;
665
+ }
656
666
  if (check.check(seq.steps, _filePath)) {
657
667
  violations.push({
658
668
  ruleId: check.ruleId,
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * protocol-domain-validator.test.ts — specific violation checks 的语言门控回归
5
+ *
6
+ * 2026-08-28 语言门控(REALWORLD_C_V6.md):PLAINTEXT_AUTH_WITHOUT_TLS 的
7
+ * Web/TLS 语义在 C 真实项目上 3 FP / 0 TP(libssh 演示 1 + uftpd 采纳案例 2,
8
+ * FTP/SSH 应用层本就明文)。SSH 主机密钥规则保留全语言(libssh 演示 1 TP)。
9
+ *
10
+ * 测试避免文件系统 I/O——直接构造 SemanticSequence。
11
+ */
12
+ const vitest_1 = require("vitest");
13
+ const protocol_domain_validator_1 = require("./protocol-domain-validator");
14
+ /** PLAINTEXT 触发序列:auth 凭证 + 网络发送 + 无 TLS 保护 */
15
+ function plaintextSequence() {
16
+ return {
17
+ steps: [
18
+ { api: "create_credentials", domain: "auth_cred", description: "", source: "lookup" },
19
+ { api: "network_send", domain: "conn_mgmt", description: "", source: "lookup" },
20
+ ],
21
+ domains: ["auth_cred", "conn_mgmt"],
22
+ primaryDomain: "conn_mgmt",
23
+ };
24
+ }
25
+ /** SSH 无主机密钥校验触发序列:≥4 个 ssh_ops + userauth + 凭证、无 hostkey */
26
+ function sshNoHostKeySequence() {
27
+ return {
28
+ steps: [
29
+ { api: "ssh_bind_accept", domain: "ssh_ops", description: "", source: "lookup" },
30
+ { api: "ssh_handle_key_exchange", domain: "ssh_ops", description: "", source: "lookup" },
31
+ { api: "ssh_userauth_password", domain: "ssh_ops", description: "", source: "lookup" },
32
+ { api: "ssh_event_dopoll", domain: "ssh_ops", description: "", source: "lookup" },
33
+ { api: "password_compare", domain: "auth_cred", description: "", source: "lookup" },
34
+ ],
35
+ domains: ["ssh_ops", "auth_cred"],
36
+ primaryDomain: "ssh_ops",
37
+ };
38
+ }
39
+ (0, vitest_1.describe)("protocol-domain-validator language gating", () => {
40
+ (0, vitest_1.it)("PLAINTEXT_AUTH_WITHOUT_TLS fires for typescript", () => {
41
+ const v = (0, protocol_domain_validator_1.checkSpecificViolations)(plaintextSequence(), undefined, "typescript");
42
+ (0, vitest_1.expect)(v.map((x) => x.ruleId)).toContain("PLAINTEXT_AUTH_WITHOUT_TLS");
43
+ });
44
+ (0, vitest_1.it)("PLAINTEXT_AUTH_WITHOUT_TLS fires for python", () => {
45
+ const v = (0, protocol_domain_validator_1.checkSpecificViolations)(plaintextSequence(), undefined, "python");
46
+ (0, vitest_1.expect)(v.map((x) => x.ruleId)).toContain("PLAINTEXT_AUTH_WITHOUT_TLS");
47
+ });
48
+ (0, vitest_1.it)("PLAINTEXT_AUTH_WITHOUT_TLS does NOT fire for C (language gate)", () => {
49
+ const v = (0, protocol_domain_validator_1.checkSpecificViolations)(plaintextSequence(), undefined, "c");
50
+ (0, vitest_1.expect)(v.map((x) => x.ruleId)).not.toContain("PLAINTEXT_AUTH_WITHOUT_TLS");
51
+ });
52
+ (0, vitest_1.it)("SSH_NO_HOST_KEY_CHECK still fires for C (ungated — libssh TP 保留)", () => {
53
+ const v = (0, protocol_domain_validator_1.checkSpecificViolations)(sshNoHostKeySequence(), undefined, "c");
54
+ (0, vitest_1.expect)(v.map((x) => x.ruleId)).toContain("SSH_NO_HOST_KEY_CHECK");
55
+ });
56
+ (0, vitest_1.it)("undefined language keeps legacy behavior (all checks run)", () => {
57
+ const v = (0, protocol_domain_validator_1.checkSpecificViolations)(plaintextSequence());
58
+ (0, vitest_1.expect)(v.map((x) => x.ruleId)).toContain("PLAINTEXT_AUTH_WITHOUT_TLS");
59
+ });
60
+ });
@@ -50,6 +50,7 @@ var __importStar = (this && this.__importStar) || (function () {
50
50
  };
51
51
  })();
52
52
  Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.normalizeName = normalizeName;
53
54
  exports.validateSequenceWithSSG = validateSequenceWithSSG;
54
55
  exports.ssgViolationsToTrustViolations = ssgViolationsToTrustViolations;
55
56
  exports.loadProjectAliases = loadProjectAliases;
@@ -250,29 +251,32 @@ function inferRuleName(apiName, domain, description, ruleNames, namespace, alias
250
251
  if (ruleName)
251
252
  return ruleName;
252
253
  }
253
- // Strategy 1: Exact normalized match against rule names
254
- for (const ruleName of ruleNames) {
255
- if (normalized === ruleName || lowerApi === ruleName) {
256
- return ruleName;
257
- }
258
- }
259
- // Strategy 2: Word-segment match — only match on complete word boundaries.
260
- // Split both the rule name and normalized call name into word segments
261
- // (separated by _). Match if every word in the rule name appears as a
262
- // word segment in the normalized call name. This prevents "status" from
263
- // matching "poll_status" via raw substring, since "status" must appear
264
- // as a complete _-delimited segment.
265
- //
266
- // 门控(P4.6.1):词段匹配只对「项目函数」适用——它是为改名协议原语设计的
267
- // (协议原语必然是项目内函数,如 S5 的 create_active_session)。外部库调用
268
- // (如 Node 的 readFileSync)经词段撞上 read_file 是纯噪声:外部 API 的语义
269
- // 桥接走 alias 配置(Strategy 0b)或 domain 关键词(Strategy 3),不走词段。
254
+ // 门控判定(P4.6.1 词段门控同款):项目函数才能按规范化/词段形态命中——
255
+ // 外部 API(如 Windows ReadFile → snake_case 撞上 read_file)是纯噪声,
256
+ // 其语义桥接走 alias 配置(Strategy 0b)或 domain 关键词(Strategy 3)。
270
257
  // 未提供 projectFunctions 时保持旧行为(向后兼容测试与无 IR 的调用方)。
271
258
  const isProjectFn = !projectFunctions
272
259
  || projectFunctions.has(apiName)
273
260
  || projectFunctions.has(lowerApi)
274
261
  || projectFunctions.has(normalized)
275
262
  || (dotIdx >= 0 && projectFunctions.has(lowerApi.slice(dotIdx + 1)));
263
+ // Strategy 1: Exact match against rule names.
264
+ // 原始名(含小写)精确匹配不限门控;规范化形态(CamelCase → snake_case)
265
+ // 仅项目函数适用——真实 C 语料验证中 ReadFile/WriteFile/DeleteFile 经
266
+ // normalized 撞上 read_file/write_file/delete_file 是 11/24 FP 的主导源,
267
+ // 而注解桥接(ACLCheckAllPerm → acl_check_all_perm)全是项目函数,不受影响。
268
+ for (const ruleName of ruleNames) {
269
+ if (lowerApi === ruleName) {
270
+ return ruleName;
271
+ }
272
+ }
273
+ if (isProjectFn) {
274
+ for (const ruleName of ruleNames) {
275
+ if (normalized === ruleName) {
276
+ return ruleName;
277
+ }
278
+ }
279
+ }
276
280
  for (const ruleName of ruleNames) {
277
281
  if (projectFunctions && !isProjectFn)
278
282
  continue;
@@ -318,7 +322,12 @@ function inferRuleName(apiName, domain, description, ruleNames, namespace, alias
318
322
  */
319
323
  function validateSequenceWithSSG(steps, rules, namespaceInitialStates, file, aliasIndex, wildcardAliases,
320
324
  /** 项目函数名集合(含裸名/全名/小写变体由调用方构造)——提供后词段匹配只对项目函数适用 */
321
- projectFunctions) {
325
+ projectFunctions,
326
+ /** 入口函数的直接调用集合——endState 检查的资源获取溯源:经内联 helper
327
+ * 获取的资源不归因给入口(nginx 回调式生命周期:open 在 helper 链内、
328
+ * close 在回调里,直接调用序列看不到),提供后仅在获取调用 ∈ 直接调用
329
+ * 时报告 endState。未提供时保持旧行为。 */
330
+ entryDirectCalls) {
322
331
  const allRuleNames = Array.from(rules.keys());
323
332
  const aliases = aliasIndex || new Map();
324
333
  const wildcards = wildcardAliases || new Map();
@@ -358,14 +367,15 @@ projectFunctions) {
358
367
  }
359
368
  })();
360
369
  // 本序列中"新获取"的资源状态(endState 检查只针对本序列获取、未释放的状态——
361
- // 继承自命名空间初始状态的不算泄漏,与 planner 语义一致)
362
- const acquiredStates = new Set();
363
- const trackAcquiredStates = (before, after) => {
370
+ // 继承自命名空间初始状态的不算泄漏,与 planner 语义一致)。
371
+ // 记录获取调用名:endState 归因溯源(入口直接调用 vs 内联 helper 获取)。
372
+ const acquiredStates = new Map(); // `${ns}::${state}` → 获取调用名
373
+ const trackAcquiredStates = (before, after, acquiringCall) => {
364
374
  for (const ns of Object.keys(after)) {
365
375
  const prev = before?.[ns] || [];
366
376
  for (const s of after[ns]) {
367
377
  if (!prev.includes(s))
368
- acquiredStates.add(`${ns}::${s}`);
378
+ acquiredStates.set(`${ns}::${s}`, acquiringCall);
369
379
  }
370
380
  }
371
381
  };
@@ -412,7 +422,7 @@ projectFunctions) {
412
422
  // Advance state
413
423
  ctx.ledger.push(result.transition);
414
424
  ctx.currentState = result.transition.statesAfter;
415
- trackAcquiredStates(result.transition.statesBefore, result.transition.statesAfter);
425
+ trackAcquiredStates(result.transition.statesBefore, result.transition.statesAfter, step.api);
416
426
  }
417
427
  else {
418
428
  violatedCalls++;
@@ -421,6 +431,8 @@ projectFunctions) {
421
431
  let fixPath = [];
422
432
  try {
423
433
  fixPath = (0, ssg_validator_1.findFixPathStatic)(rules, ruleNamespace, rejection.currentState, rejection.requiredState);
434
+ // 修复路径渲染真实函数名:项目注解原语的 displayName 优先于通用规则名
435
+ fixPath = fixPath.map((n) => rules.get(n)?.displayName ?? n);
424
436
  }
425
437
  catch {
426
438
  fixPath = rejection.missingFunctions || [];
@@ -442,7 +454,7 @@ projectFunctions) {
442
454
  // Still advance state on rejection (best-effort: apply transition anyway
443
455
  // so subsequent calls can be validated)
444
456
  ctx.ledger.push(result.transition);
445
- trackAcquiredStates(result.transition.statesBefore, result.transition.statesAfter);
457
+ trackAcquiredStates(result.transition.statesBefore, result.transition.statesAfter, step.api);
446
458
  }
447
459
  }
448
460
  catch {
@@ -476,24 +488,31 @@ projectFunctions) {
476
488
  const ctx = contexts.get(hs.namespace);
477
489
  if (!ctx)
478
490
  continue;
479
- if (!acquiredStates.has(`${hs.namespace}::${hs.state}`))
491
+ const acquiringCall = acquiredStates.get(`${hs.namespace}::${hs.state}`);
492
+ if (!acquiringCall)
493
+ continue;
494
+ // 资源获取溯源:经内联 helper 获取的状态不归因给入口(释放可能存在于
495
+ // 兄弟 helper 或回调注册中——nginx 回调式生命周期是 12/24 FP 的主源)
496
+ if (entryDirectCalls && !entryDirectCalls.has(acquiringCall))
480
497
  continue;
481
498
  const cur = ctx.currentState[hs.namespace] || [];
482
499
  if (!cur.includes(hs.state))
483
500
  continue;
484
501
  violatedCalls++;
502
+ // 释放函数渲染真实名(项目注解原语的 displayName 优先)
503
+ const releaseDisplay = rules.get(hs.releaseFn)?.displayName ?? hs.releaseFn;
485
504
  violations.push({
486
505
  callName: "(end-of-sequence)",
487
506
  namespace: hs.namespace,
488
507
  currentState: cur,
489
508
  requiredState: [],
490
- fixPath: [hs.releaseFn],
509
+ fixPath: [releaseDisplay],
491
510
  matchedRule: hs.releaseFn,
492
511
  endState: true,
493
512
  explanation: `SSG end-state violation: resource state [${hs.state}] ` +
494
513
  `acquired in this function is still held at end of sequence ` +
495
514
  `(namespace ${hs.namespace}, current [${cur.join(", ")}]) — ` +
496
- `missing release call: ${hs.releaseFn}.`,
515
+ `missing release call: ${releaseDisplay}.`,
497
516
  });
498
517
  trace.push({
499
518
  call: "(end-of-sequence)",
@@ -673,6 +692,32 @@ function loadProtocolRules(projectPath) {
673
692
  nsInit._global = "INIT";
674
693
  if (!nsInit.stateless)
675
694
  nsInit.stateless = "IDLE";
695
+ // ── Load shared C alias registry (c-aliases.json, confirmed entries only) ──
696
+ // 注解驱动定位的孵化器燃料:库边界别名跨项目迁移——用户项目别名回写提案、
697
+ // 人工确认(status=confirmed)后对全部项目生效。加载顺序在项目别名之前:
698
+ // 项目本地映射优先于共享表(与「项目别名不覆盖全局」同哲学,first-wins)。
699
+ try {
700
+ const cRegistryPath = [
701
+ projectPath ? path.join(projectPath, "c-aliases.json") : "",
702
+ path.join(process.cwd(), "c-aliases.json"),
703
+ path.join(__dirname, "..", "c-aliases.json"),
704
+ ].find((p) => p && fs.existsSync(p));
705
+ if (cRegistryPath) {
706
+ const reg = JSON.parse(fs.readFileSync(cRegistryPath, "utf-8"));
707
+ for (const entry of reg.entries || []) {
708
+ if (entry.status !== "confirmed")
709
+ continue;
710
+ const callName = String(entry.call).toLowerCase().trim();
711
+ const ruleName = String(entry.rule);
712
+ if (!callName || !rules.has(ruleName))
713
+ continue;
714
+ if (!aliasIndex.has(callName)) {
715
+ aliasIndex.set(callName, ruleName);
716
+ }
717
+ }
718
+ }
719
+ }
720
+ catch { /* best-effort — 共享表缺失或损坏不影响验证 */ }
676
721
  // ── Load project-level aliases (supplemental, never override global) ──
677
722
  let projectAliases;
678
723
  let aliasWarnings;
@@ -8,8 +8,44 @@
8
8
  * 3. Detects state machine violations
9
9
  * 4. Passes valid sequences without false positives
10
10
  */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
11
44
  Object.defineProperty(exports, "__esModule", { value: true });
12
45
  const vitest_1 = require("vitest");
46
+ const fs = __importStar(require("fs"));
47
+ const os = __importStar(require("os"));
48
+ const path = __importStar(require("path"));
13
49
  const ssg_bridge_1 = require("./ssg-bridge");
14
50
  // Load protocol rules once for all tests
15
51
  const protoData = (0, ssg_bridge_1.loadProtocolRules)();
@@ -272,3 +308,43 @@ function step(api, domain, description = "") {
272
308
  });
273
309
  });
274
310
  });
311
+ (0, vitest_1.describe)("共享 C 别名表(c-aliases.json,孵化器机制)", () => {
312
+ (0, vitest_1.it)("confirmed 条目被加载进 aliasIndex;proposed 条目不加载(人工确认门)", () => {
313
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-calias-"));
314
+ try {
315
+ fs.writeFileSync(path.join(dir, "c-aliases.json"), JSON.stringify({
316
+ entries: [
317
+ { call: "ssh_userauth_password", rule: "verify_password", status: "confirmed" },
318
+ { call: "some_lib_connect", rule: "connect_db", status: "proposed" },
319
+ { call: "ghost_call", rule: "no_such_rule", status: "confirmed" },
320
+ ],
321
+ }));
322
+ const loaded = (0, ssg_bridge_1.loadProtocolRules)(dir);
323
+ (0, vitest_1.expect)(loaded).not.toBeNull();
324
+ (0, vitest_1.expect)(loaded.aliasIndex.get("ssh_userauth_password")).toBe("verify_password");
325
+ (0, vitest_1.expect)(loaded.aliasIndex.has("some_lib_connect")).toBe(false); // 未确认不生效
326
+ (0, vitest_1.expect)(loaded.aliasIndex.has("ghost_call")).toBe(false); // 规则不存在跳过
327
+ }
328
+ finally {
329
+ fs.rmSync(dir, { recursive: true, force: true });
330
+ }
331
+ });
332
+ (0, vitest_1.it)("共享表不覆盖全局别名与项目别名(first-wins)", () => {
333
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-calias2-"));
334
+ try {
335
+ // 全局 verify_hash 的别名 jsonwebtoken.verify——共享表同名条目不得覆盖
336
+ fs.writeFileSync(path.join(dir, "c-aliases.json"), JSON.stringify({
337
+ entries: [
338
+ { call: "jsonwebtoken.verify", rule: "verify_password", status: "confirmed" },
339
+ ],
340
+ }));
341
+ const loaded = (0, ssg_bridge_1.loadProtocolRules)(dir);
342
+ const globalRule = loaded.rules.get("verify_token")?.aliases?.includes("jsonwebtoken.verify");
343
+ // jsonwebtoken.verify 若已存在全局别名,共享表不覆盖(值保持原规则)
344
+ (0, vitest_1.expect)(loaded.aliasIndex.get("jsonwebtoken.verify")).not.toBe("verify_password");
345
+ }
346
+ finally {
347
+ fs.rmSync(dir, { recursive: true, force: true });
348
+ }
349
+ });
350
+ });
@@ -174,7 +174,7 @@
174
174
  本文档面向两类读者:想快速理解的初学者,与需要专业判断的投资人。</div>
175
175
  <div class="meta">
176
176
  <span>📄 版本 v1.0 · 2026-08</span>
177
- <span>🧪 引擎版本 v3.7.2</span>
177
+ <span>🧪 引擎版本 v<span id="npmVersion">3.7.6</span></span>
178
178
  <span>📊 13/13 安全类别覆盖</span>
179
179
  <span>🔓 开源 MIT</span>
180
180
  <span>📦 npm install progmune-runtime</span>
@@ -724,5 +724,24 @@ AI 生成: create_session(...) ← 当前状态 UNAUTHENTICATED
724
724
  </div>
725
725
 
726
726
  </div>
727
+
728
+ <script>
729
+ // ═══ 引擎版本号动态展示:registry.npmjs.org dist-tags.latest ═══
730
+ // 发布到 npm 后自动跟随最新版本;拉取失败保留静态回退值。
731
+ (async function loadNpmVersion() {
732
+ const el = document.getElementById("npmVersion");
733
+ if (!el) return;
734
+ const ctrl = new AbortController();
735
+ const timer = setTimeout(() => ctrl.abort(), 8000);
736
+ try {
737
+ const res = await fetch("https://registry.npmjs.org/progmune-runtime", { signal: ctrl.signal });
738
+ clearTimeout(timer);
739
+ if (!res.ok) throw new Error(String(res.status));
740
+ const pkg = await res.json();
741
+ const latest = pkg && pkg["dist-tags"] && pkg["dist-tags"].latest;
742
+ if (latest) el.textContent = latest;
743
+ } catch { /* 保留静态回退值 */ }
744
+ })();
745
+ </script>
727
746
  </body>
728
747
  </html>
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.4",
3
+ "version": "3.7.6",
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/",
7
7
  "protocols.json",
8
+ "c-aliases.json",
8
9
  "CHANGELOG.md",
9
10
  "docs/Progmune_项目全解.html",
10
11
  "docs/Progmune_投资人白皮书_v2.0.html"