progmune-runtime 3.7.11 → 3.7.13

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,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.13] — 2026-09-02
4
+
5
+ ### Go 语言支持 + Gin/Fiber 框架适配——12/13
6
+
7
+ - **Go IR 提取器**(`src/extract-ir-go.ts`,纯 TS 词法——与 C 提取器同哲学:零外部工具链,npm 安装态可用,不重蹈 Python 桥的安装态覆辙):func 签名(多行/接收者方法)、调用提取(obj.Method() 取 Method)、注释注解 `// @progmune(...)` + 文档标签、exported=首字母大写、vendor/testdata/*_test.go 表面过滤、Go 关键字排除、反引号 raw string 掩码;8 个单测
8
+ - **注册表接入**:LANGUAGE_EXTRACTORS 第 4 语言(detect .go → extractIRGo)→ extractProjectIR/evaluateTrust 全链路自动生效;**引擎 autoExtractor 补 go**(修复 Go 项目静默走正则回退的同类陷阱——首轮金标全错暴露)
9
+ - **Go 协议盲测 v1**(`scan-protocol-golang.ts`,自包含生成+扫描):3 干净(直连/方法/helper)× 3 植入违规(missing_auth/read_without_open/leak)——**TP 5 / FP 0 / FN 0 → P=100% / R=100%**
10
+ - **Gin(第 11 个)**:`gin-detector.ts`——GIN_ROUTE_NO_AUTH(r.POST 中间件链 + Use/Group 组级认证);Go 方法名大写(r.POST)正则 i 标志修复;"middleware" 词误撞修复(loggerMiddleware 类工具中间件≠认证)
11
+ - **Fiber(第 12 个)**:`fiber-detector.ts`——FIBER_ROUTE_NO_AUTH(app.Post 中间件链 + Use);同款修复
12
+ - **引擎**:collectGinViolations/collectFiberViolations(Go 目录结构特殊:cmd/internal/pkg + 根目录 main.go)+ ginCoverage/fiberCoverage
13
+ - **文档**:Go 语言行 ❌→✅ 注解驱动(Beta)(README 双语/矩阵双语/CLAUDE.md/落地页 + i18n);框架 **12/13** 口径(剩余 Spring Boot 需 Java 支持先行)
14
+ - **验证**:23 个新单测(Go 提取器 8 + Gin 8 + Fiber 7);全套件 **204/204**;Python 盲测 v1.2 64 零漂移;C 演示不变;check 免疫正常
15
+
16
+ ## [3.7.12] — 2026-09-01
17
+
18
+ ### Koa + Hapi 框架结构适配——10/13
19
+
20
+ - **Koa(第 9 个)**:`src/frameworks/koa-detector.ts`(代码串级,镜像 express/koa 中间件模式)——`KOA_ROUTE_NO_AUTH`:mutation 路由注册中间件链无认证名中间件,且文件内无认证 `app.use` 全局中间件;认证名按词表分类(auth/login/permission/jwt/verify/guard…);GET 读操作与认证入口路径豁免
21
+ - **Hapi(第 10 个)**:`src/frameworks/hapi-detector.ts`(路由配置模式)——`HAPI_ROUTE_NO_AUTH`:mutation 路由 options 无 auth 字段或显式 `auth: false`(显式公开 mutation 检出);`auth: 'strategy'` / `auth: { strategy: 'x' }` 两种形态均识别为受保护;`auth.strategy` 声明被记录
22
+ - **引擎接线**:collectKoaViolations / collectHapiViolations + overall `koaCoverage`/`hapiCoverage`(加性 best-effort);frameworks barrel 补全(11 个适配器模块)
23
+ - **验证**:15 个新单测(koa 7 + hapi 8,含显式 auth:false 与 strategy 对象形态);引擎冒烟:Koa 全局认证中间件 0 误报 APPROVED 90、Hapi 无 auth 路由检出 + login 豁免 APPROVED 87;全套件 **181/181**;Python 盲测 v1.2 64 零漂移
24
+ - **文档**:10/13 口径(README 双语/覆盖矩阵双语/CLAUDE.md/落地页 + Koa/Hapi 矩阵行)
25
+ - **边界(如实)**:文件级窗口(跨文件全局中间件不可见,与 Express 检测器同款);认证中间件按名词语汇识别;Hapi 路由窗口 500 字符(超长配置截断)
26
+
3
27
  ## [3.7.11] — 2026-09-01
4
28
 
5
29
  ### NestJS 补全——框架适配 8/13
package/README.md CHANGED
@@ -104,11 +104,12 @@ Progmune is honest about what it can and cannot verify.
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
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
- | **Go, Java** | None | Planned |
107
+ | **Go** | Annotation-driven (Beta) | IR extraction via registry + SSG state machine; synthetic gold v1: **P=100% / R=100%** (clean×3 + violations×3); zero external toolchain (pure-TS lexical extractor — works from npm installs) |
108
+ | **Java** | ❌ None | Planned |
108
109
 
109
110
  **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.
110
111
 
111
- **Framework adapters: 8/13.** Express ✅, tRPC ✅, FastAPI ✅, Django ✅ (structural route-level auth analysis — every mutation route checked for auth), Flask ✅ (route/before_request auth guard analysis), Fastify ✅ (route preHandler/hook auth analysis) and Next.js ✅ (App Router route-handler auth analysis) and NestJS ✅ (decorator route parsing + global APP_GUARD recognition + @Public exemption) have dedicated detectors. Next.js version-aware governance also exists. Spring Boot/Go frameworks and a few TS frameworks remain — framework adaptation is the #1 product gap.
112
+ **Framework adapters: 12/13.** Express ✅, tRPC ✅, FastAPI ✅, Django ✅ (structural route-level auth analysis — every mutation route checked for auth), Flask ✅ (route/before_request auth guard analysis), Fastify ✅ (route preHandler/hook auth analysis) and Next.js ✅ (App Router route-handler auth analysis), NestJS ✅ (decorator route parsing + global APP_GUARD recognition + @Public exemption), Koa ✅, Hapi ✅ (route-config auth strategy analysis), Gin ✅ and Fiber ✅ (Go route/auth-middleware analysis) have dedicated detectors. Next.js version-aware governance also exists. Spring Boot/Go frameworks and a few TS frameworks remain — framework adaptation is the #1 product gap.
112
113
 
113
114
  ### What Progmune does NOT cover (honest boundaries)
114
115
 
@@ -234,7 +235,7 @@ High-impact contribution areas:
234
235
  - **Runtime Pipeline:** Detect → Explain → Repair → Validate (L1–L4)
235
236
  - **Trust Engine:** 4-dimension scoring with binary explainability gate
236
237
  - **MCP Tools:** 19 — `progmune_trust_check`, `progmune_score`, `progmune_policy_check`, `progmune_certify`, and more
237
- - **Framework Adapters:** Express ✅, tRPC ✅, FastAPI ✅, Django ✅, Flask ✅, Fastify ✅, Next.js ✅, NestJS ✅ (8/13)
238
+ - **Framework Adapters:** Express ✅, tRPC ✅, FastAPI ✅, Django ✅, Flask ✅, Fastify ✅, Next.js ✅, NestJS ✅ (12/13)
238
239
  - **Knowledge Base:** 31 domains, 148 protocol rules, 22 detectors, 26 safeguards, PLSB 13/13 categories — plus 15 source-level detection rules (Python)
239
240
  - **Corpus:** 2,500+ trajectories across 6+ repositories; blind benchmarks 100 (TS) + 90 (Python) projects; real-world validation on 4 application repos
240
241
  - **Current focus:** Enterprise PoC validation + remaining framework-internal boundary FPs
package/README.zh-CN.md CHANGED
@@ -104,11 +104,12 @@ Progmune 对能验证什么、不能验证什么保持诚实。
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
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
- | **Go, Java** | | 规划中 |
107
+ | **Go** | 注解驱动(Beta) | IR 提取接入注册表 + SSG 状态机;合成金标 v1:**P=100% / R=100%**(3 干净 × 3 植入违规);零外部工具链(纯 TS 词法提取,npm 安装态可用) |
108
+ | **Java** | ❌ 无 | 规划中 |
108
109
 
109
110
  **多语言 IR(注册表式)。** TypeScript(ts-morph)与 Python(AST)提取器是同一注册表(`src/extract-project-ir.ts`)中的注册项;`extractProjectIR` 把检测到的所有语言合并为一份函数 IR,由 agent loop、`execute()` 与 MCP server 共享——agent 可在两种语言上编排函数协议链。新增语言(Go、Java……)= 注册一条提取器,调用方零改动。
110
111
 
111
- **框架适配器:8/13。** Express ✅、tRPC ✅、FastAPI ✅、Django ✅(结构级路由认证分析——逐条检查写操作入口的认证)、Flask ✅(路由/before_request 认证守卫分析)、Fastify ✅(路由 preHandler/钩子认证分析)与 Next.js ✅(App Router 路由处理器认证分析)与 NestJS ✅(装饰器路由解析 + 全局 APP_GUARD 守卫识别 + @Public 豁免)有专用检测器;Next.js 另有版本感知治理。Spring Boot、Go 框架及少数 TS 框架待适配——框架适配是 #1 产品缺口。
112
+ **框架适配器:12/13。** Express ✅、tRPC ✅、FastAPI ✅、Django ✅(结构级路由认证分析——逐条检查写操作入口的认证)、Flask ✅(路由/before_request 认证守卫分析)、Fastify ✅(路由 preHandler/钩子认证分析)与 Next.js ✅(App Router 路由处理器认证分析)、NestJS ✅(装饰器路由解析 + 全局 APP_GUARD 守卫识别 + @Public 豁免)、Koa ✅、Hapi ✅(路由配置认证策略分析)、Gin ✅ 与 Fiber ✅(Go 路由/认证中间件分析)有专用检测器;Next.js 另有版本感知治理。Spring Boot、Go 框架及少数 TS 框架待适配——框架适配是 #1 产品缺口。
112
113
 
113
114
  ### Progmune 不覆盖什么(诚实边界)
114
115
 
@@ -233,7 +234,7 @@ Progmune 建立在"**LLM 输出是统计表演而非推理**"这一前提上—
233
234
  - **运行时管线:** 检测 → 解释 → 修复 → 验证(L1–L4)
234
235
  - **信任引擎:** 四维评分 + 二元可解释性门
235
236
  - **MCP 工具:** 19 个——`progmune_trust_check`、`progmune_score`、`progmune_policy_check`、`progmune_certify` 等
236
- - **框架适配器:** Express ✅、tRPC ✅、FastAPI ✅、Django ✅、Flask ✅、Fastify ✅、Next.js ✅、NestJS ✅(8/13)
237
+ - **框架适配器:** Express ✅、tRPC ✅、FastAPI ✅、Django ✅、Flask ✅、Fastify ✅、Next.js ✅、NestJS ✅(12/13)
237
238
  - **知识库:** 31 个域、148 条协议规则、22 个检测器、26 条防护规则、PLSB 13/13 类别——另加 15 条源码级检测规则(Python)
238
239
  - **语料:** 6+ 仓库 2,500+ 轨迹;盲测基准 100(TS)+ 90(Python)个项目;4 个应用仓库真实验证
239
240
  - **当前重点:** 企业 PoC 验证 + 剩余框架内部边界误报
@@ -0,0 +1,383 @@
1
+ "use strict";
2
+ /**
3
+ * Go IR Extractor — pure-TS function extraction for the merged multi-language
4
+ * registry (extract-project-ir.ts LANGUAGE_EXTRACTORS)。无子进程、无 Go
5
+ * 工具链依赖(npm 安装态可用——与 C 提取器同哲学,区别于 Python 桥)。
6
+ *
7
+ * Route: the registry "go" entry calls extractIRGo; the result merges into
8
+ * the same FunctionInfo list as TypeScript/Python/C and flows into
9
+ * call-sequence (P4.6), SSG validation and the agent loop with no rewiring.
10
+ *
11
+ * Extraction is a best-effort lexical parse of Go definitions:
12
+ * - func Name(params) returns { ... } 普通函数
13
+ * - func (r *Repo) Name(params) returns { } 接收者方法(方法名=Name)
14
+ * - 多行签名(平衡括号)、返回值类型 best-effort 记录
15
+ * - 直接调用提取(obj.Method() 取 Method——与 C 成员调用同规则)
16
+ * - 注释注解:// @progmune(namespace=..., pre=[...], post=[...])
17
+ * + @purpose/@tags/@requires/@produces/@useWhen/@inputs/@outputs 文档标签
18
+ * - exported = 首字母大写(Go 语言约定)
19
+ *
20
+ * Known limits(如实):
21
+ * - 接口方法/函数值分发静态不可见(interface 与 func 变量同 L3 边界)
22
+ * - 泛型类型参数(func F[T any](...))best-effort 剥除,不做完整解析
23
+ * - 无数据流/指针分析——与 C 的 L3/L4 结论同源
24
+ */
25
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
26
+ if (k2 === undefined) k2 = k;
27
+ var desc = Object.getOwnPropertyDescriptor(m, k);
28
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
29
+ desc = { enumerable: true, get: function() { return m[k]; } };
30
+ }
31
+ Object.defineProperty(o, k2, desc);
32
+ }) : (function(o, m, k, k2) {
33
+ if (k2 === undefined) k2 = k;
34
+ o[k2] = m[k];
35
+ }));
36
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
37
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
38
+ }) : function(o, v) {
39
+ o["default"] = v;
40
+ });
41
+ var __importStar = (this && this.__importStar) || (function () {
42
+ var ownKeys = function(o) {
43
+ ownKeys = Object.getOwnPropertyNames || function (o) {
44
+ var ar = [];
45
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
46
+ return ar;
47
+ };
48
+ return ownKeys(o);
49
+ };
50
+ return function (mod) {
51
+ if (mod && mod.__esModule) return mod;
52
+ var result = {};
53
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
54
+ __setModuleDefault(result, mod);
55
+ return result;
56
+ };
57
+ })();
58
+ Object.defineProperty(exports, "__esModule", { value: true });
59
+ exports.extractIRGo = extractIRGo;
60
+ const fs = __importStar(require("fs"));
61
+ const path = __importStar(require("path"));
62
+ // ── Constants ──
63
+ const GO_EXTENSIONS = new Set([".go"]);
64
+ /** Walk skip set(生产表面守卫,与 C/Python 提取器口径一致)。 */
65
+ const SKIP_DIRS = new Set([
66
+ "vendor", "testdata", "test", "tests", "examples", "example", "docs",
67
+ "doc", "third_party", "node_modules", ".git", "scripts", "tools",
68
+ ]);
69
+ /** Go 关键字/内置(调用提取时排除)。 */
70
+ const GO_KEYWORDS = new Set([
71
+ "break", "case", "chan", "const", "continue", "default", "defer", "else",
72
+ "fallthrough", "for", "func", "go", "goto", "if", "import", "interface",
73
+ "map", "package", "range", "return", "select", "struct", "switch", "type",
74
+ "var", "append", "cap", "close", "complex", "copy", "delete", "imag",
75
+ "len", "make", "new", "panic", "print", "println", "real", "recover",
76
+ ]);
77
+ function isTestFilename(name) {
78
+ return name.endsWith("_test.go");
79
+ }
80
+ function maskLine(line, state) {
81
+ let out = "";
82
+ for (let i = 0; i < line.length; i++) {
83
+ const ch = line[i];
84
+ const next = line[i + 1];
85
+ if (state.inBlock) {
86
+ if (ch === "*" && next === "/") {
87
+ state.inBlock = false;
88
+ i++;
89
+ }
90
+ out += " ";
91
+ }
92
+ else if (state.inString) {
93
+ if (ch === "\\") {
94
+ out += " ";
95
+ i++;
96
+ }
97
+ else if (ch === '"') {
98
+ state.inString = false;
99
+ out += " ";
100
+ }
101
+ else
102
+ out += " ";
103
+ }
104
+ else if (state.inRaw) {
105
+ if (ch === "`") {
106
+ state.inRaw = false;
107
+ out += " ";
108
+ }
109
+ else
110
+ out += " ";
111
+ }
112
+ else if (state.inRune) {
113
+ if (ch === "\\") {
114
+ out += " ";
115
+ i++;
116
+ }
117
+ else if (ch === "'") {
118
+ state.inRune = false;
119
+ out += " ";
120
+ }
121
+ else
122
+ out += " ";
123
+ }
124
+ else {
125
+ if (ch === "/" && next === "*") {
126
+ state.inBlock = true;
127
+ i++;
128
+ out += " ";
129
+ }
130
+ else if (ch === "/" && next === "/") {
131
+ out += " ";
132
+ break;
133
+ }
134
+ else if (ch === '"') {
135
+ state.inString = true;
136
+ out += " ";
137
+ }
138
+ else if (ch === "`") {
139
+ state.inRaw = true;
140
+ out += " ";
141
+ }
142
+ else if (ch === "'") {
143
+ state.inRune = true;
144
+ out += " ";
145
+ }
146
+ else
147
+ out += ch;
148
+ }
149
+ }
150
+ return out;
151
+ }
152
+ function parseGoAnnotation(text) {
153
+ const m = text.match(/@progmune\s*\(\s*([^)]*)\)/);
154
+ if (!m)
155
+ return null;
156
+ const body = m[1];
157
+ const ann = { pre_states: [], post_states: [] };
158
+ const ns = body.match(/namespace\s*=\s*"([^"]+)"/);
159
+ if (ns)
160
+ ann.namespace = ns[1];
161
+ const states = (key) => {
162
+ const mm = body.match(new RegExp(`${key}\\s*=\\s*\\[([^\\]]*)\\]`));
163
+ if (!mm)
164
+ return [];
165
+ const vals = mm[1].match(/"([^"]+)"/g) || [];
166
+ return vals.map((v) => v.slice(1, -1));
167
+ };
168
+ ann.pre_states = states("pre");
169
+ ann.post_states = states("post");
170
+ const inv = states("invalidate");
171
+ if (inv.length > 0)
172
+ ann.invalidate = inv;
173
+ return ann;
174
+ }
175
+ function docTag(text, tag) {
176
+ const m = text.match(new RegExp(`@${tag}\\s+([^\\n*]+)`));
177
+ return m ? m[1].trim() : undefined;
178
+ }
179
+ // ── 调用提取 ──
180
+ function extractCallsFromBody(body) {
181
+ const calls = new Set();
182
+ const re = /([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
183
+ let m;
184
+ while ((m = re.exec(body)) !== null) {
185
+ const name = m[1];
186
+ if (GO_KEYWORDS.has(name))
187
+ continue;
188
+ if (GO_KEYWORDS.has(name))
189
+ continue;
190
+ calls.add(name);
191
+ }
192
+ return Array.from(calls);
193
+ }
194
+ // ── 顶层参数切分 ──
195
+ function splitTopLevelParams(s) {
196
+ const parts = [];
197
+ let depth = 0;
198
+ let cur = "";
199
+ for (const ch of s) {
200
+ if (ch === "(" || ch === "[" || ch === "{")
201
+ depth++;
202
+ if (ch === ")" || ch === "]" || ch === "}")
203
+ depth--;
204
+ if (ch === "," && depth === 0) {
205
+ parts.push(cur.trim());
206
+ cur = "";
207
+ }
208
+ else
209
+ cur += ch;
210
+ }
211
+ if (cur.trim())
212
+ parts.push(cur.trim());
213
+ return parts.filter(Boolean);
214
+ }
215
+ // ── 单文件提取 ──
216
+ function extractGoFile(filePath) {
217
+ let content;
218
+ try {
219
+ content = fs.readFileSync(filePath, "utf-8");
220
+ }
221
+ catch {
222
+ return [];
223
+ }
224
+ const lines = content.split("\n");
225
+ const functions = [];
226
+ for (let i = 0; i < lines.length; i++) {
227
+ // 函数定义行:func Name( 或 func (receiver) Name(
228
+ const plain = lines[i].match(/^\s*func\s+([A-Za-z_][A-Za-z0-9_]*)/);
229
+ const receiver = lines[i].match(/^\s*func\s*\([^)]*\)\s*([A-Za-z_][A-Za-z0-9_]*)/);
230
+ const name = plain ? plain[1] : receiver ? receiver[1] : null;
231
+ if (!name)
232
+ continue;
233
+ // 多行签名:从 func 起拼接直到找到 { 或 ;
234
+ let sigText = lines[i];
235
+ let j = i;
236
+ while (!sigText.includes("{") && !sigText.includes(";") && j < lines.length - 1) {
237
+ j++;
238
+ sigText += "\n" + lines[j];
239
+ }
240
+ const braceIdx = sigText.indexOf("{");
241
+ if (braceIdx < 0)
242
+ continue; // 接口方法声明(无函数体)
243
+ const signature = sigText.slice(0, braceIdx);
244
+ // 注释块注解:函数定义行上方的连续注释(// 与 /* */)
245
+ const commentLines = [];
246
+ for (let k = i - 1; k >= 0; k--) {
247
+ const t = lines[k].trim();
248
+ if (t.startsWith("//") || t.startsWith("/*") || t.startsWith("*")) {
249
+ commentLines.unshift(t);
250
+ }
251
+ else if (t === "") {
252
+ if (commentLines.length === 0)
253
+ continue;
254
+ break;
255
+ }
256
+ else {
257
+ break;
258
+ }
259
+ }
260
+ const commentText = commentLines.join("\n");
261
+ // 注解解析
262
+ let protocol;
263
+ const ann = parseGoAnnotation(commentText);
264
+ if (ann) {
265
+ protocol = {
266
+ pre_states: ann.pre_states,
267
+ post_states: ann.post_states,
268
+ ...(ann.invalidate ? { invalidate: ann.invalidate } : {}),
269
+ ...(ann.namespace ? { namespace: ann.namespace } : {}),
270
+ };
271
+ }
272
+ const purpose = docTag(commentText, "purpose");
273
+ const tags = docTag(commentText, "tags")?.split(/[,\s]+/).filter(Boolean);
274
+ const requires = docTag(commentText, "requires")?.split(/[,\s]+/).filter(Boolean);
275
+ const produces = docTag(commentText, "produces")?.split(/[,\s]+/).filter(Boolean);
276
+ const useWhen = docTag(commentText, "useWhen")?.split(/[,\s]+/).filter(Boolean);
277
+ // 参数列表:签名中第一个 ( ... )
278
+ const openParen = signature.indexOf("(");
279
+ const closeParen = signature.indexOf(")", openParen);
280
+ let params = [];
281
+ if (openParen >= 0 && closeParen > openParen) {
282
+ params = splitTopLevelParams(signature.slice(openParen + 1, closeParen));
283
+ }
284
+ // 返回类型:签名尾部(剥除泛型括号)
285
+ const returnType = signature.slice(closeParen + 1).replace(/\[[^\]]*\]/g, "").trim();
286
+ // 函数体:从签名行(j)的 { 起逐行扫描到匹配 }(掩码后括号平衡,
287
+ // 增量计数——每行只数一次,避免累积体重复计数)
288
+ let body = "";
289
+ let depth = 0;
290
+ let started = false;
291
+ let endLine = j;
292
+ const bodyMask = { inBlock: false, inString: false, inRaw: false, inRune: false };
293
+ for (let li = j; li < lines.length; li++) {
294
+ const maskedLine = maskLine(lines[li], bodyMask);
295
+ if (!started) {
296
+ const b = maskedLine.indexOf("{");
297
+ if (b < 0)
298
+ continue; // 多行签名的后续行还没到 {
299
+ started = true;
300
+ for (const ch of maskedLine.slice(b)) {
301
+ if (ch === "{")
302
+ depth++;
303
+ else if (ch === "}")
304
+ depth--;
305
+ }
306
+ }
307
+ else {
308
+ for (const ch of maskedLine) {
309
+ if (ch === "{")
310
+ depth++;
311
+ else if (ch === "}")
312
+ depth--;
313
+ }
314
+ }
315
+ body += lines[li] + "\n";
316
+ if (depth === 0) {
317
+ endLine = li;
318
+ break;
319
+ }
320
+ }
321
+ if (!started || depth !== 0)
322
+ continue;
323
+ // 调用提取(掩码态重置后重扫完整函数体)
324
+ const resetMask = { inBlock: false, inString: false, inRaw: false, inRune: false };
325
+ const maskedBody = body.split("\n").map((l) => maskLine(l, resetMask)).join("\n");
326
+ const calls = extractCallsFromBody(maskedBody).filter((c) => c !== name);
327
+ functions.push({
328
+ name,
329
+ file: filePath,
330
+ params: params.map((p) => ({ name: p.split(/[\s\[\]]/)[0] || p, type: p })),
331
+ returnType: returnType || "unknown",
332
+ calls,
333
+ exported: /^[A-Z]/.test(name),
334
+ tags: tags && tags.length > 0 ? tags : undefined,
335
+ ...(purpose ? { purpose } : {}),
336
+ ...(requires && requires.length > 0 ? { requires } : {}),
337
+ ...(produces && produces.length > 0 ? { produces } : {}),
338
+ ...(useWhen && useWhen.length > 0 ? { useWhen } : {}),
339
+ ...(protocol ? { protocol } : {}),
340
+ });
341
+ i = endLine; // 跳过已消费的行
342
+ }
343
+ return functions;
344
+ }
345
+ // ── 项目级入口(注册表调用) ──
346
+ function collectGoFiles(root) {
347
+ const files = [];
348
+ const walk = (dir) => {
349
+ let entries;
350
+ try {
351
+ entries = fs.readdirSync(dir, { withFileTypes: true });
352
+ }
353
+ catch {
354
+ return;
355
+ }
356
+ for (const e of entries) {
357
+ if (e.isSymbolicLink())
358
+ continue;
359
+ const full = path.join(dir, e.name);
360
+ if (e.isDirectory()) {
361
+ if (SKIP_DIRS.has(e.name))
362
+ continue;
363
+ walk(full);
364
+ }
365
+ else if (GO_EXTENSIONS.has(path.extname(e.name)) && !isTestFilename(e.name)) {
366
+ files.push(full);
367
+ }
368
+ }
369
+ };
370
+ walk(root);
371
+ return files;
372
+ }
373
+ function extractIRGo(projectRoot) {
374
+ const files = collectGoFiles(projectRoot);
375
+ const all = [];
376
+ for (const file of files) {
377
+ for (const fn of extractGoFile(file)) {
378
+ fn.file = path.relative(projectRoot, fn.file);
379
+ all.push(fn);
380
+ }
381
+ }
382
+ return all;
383
+ }
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ /**
37
+ * extract-ir-go.test.ts — Go IR 提取器回归(临时目录夹具)
38
+ *
39
+ * 纯 TS 词法提取(与 C 提取器同哲学):函数签名(多行/接收者方法)、
40
+ * 调用(obj.Method() 取 Method)、注释注解 @progmune + 文档标签、
41
+ * exported=首字母大写、非生产表面过滤。
42
+ */
43
+ const vitest_1 = require("vitest");
44
+ const fs = __importStar(require("fs"));
45
+ const os = __importStar(require("os"));
46
+ const path = __importStar(require("path"));
47
+ const extract_ir_go_1 = require("./extract-ir-go");
48
+ let dir;
49
+ (0, vitest_1.beforeEach)(() => {
50
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), "go-ir-"));
51
+ });
52
+ (0, vitest_1.afterEach)(() => {
53
+ fs.rmSync(dir, { recursive: true, force: true });
54
+ });
55
+ function write(rel, content) {
56
+ const full = path.join(dir, rel);
57
+ fs.mkdirSync(path.dirname(full), { recursive: true });
58
+ fs.writeFileSync(full, content);
59
+ }
60
+ (0, vitest_1.describe)("extract-ir-go", () => {
61
+ (0, vitest_1.it)("普通函数 + 调用提取(含 obj.Method() 成员调用)", () => {
62
+ write("main.go", `
63
+ package main
64
+
65
+ func doWork() {
66
+ helper()
67
+ obj.Save()
68
+ }
69
+
70
+ func helper() {}
71
+ `);
72
+ const ir = (0, extract_ir_go_1.extractIRGo)(dir);
73
+ const doWork = ir.find((f) => f.name === "doWork");
74
+ (0, vitest_1.expect)(doWork?.calls).toEqual(vitest_1.expect.arrayContaining(["helper", "Save"]));
75
+ });
76
+ (0, vitest_1.it)("接收者方法(func (r *Repo) Name)", () => {
77
+ write("repo.go", `
78
+ package main
79
+
80
+ func (r *Repo) SaveUser(u User) error {
81
+ return nil
82
+ }
83
+ `);
84
+ const ir = (0, extract_ir_go_1.extractIRGo)(dir);
85
+ (0, vitest_1.expect)(ir.some((f) => f.name === "SaveUser")).toBe(true);
86
+ (0, vitest_1.expect)(ir.find((f) => f.name === "SaveUser")?.exported).toBe(true);
87
+ });
88
+ (0, vitest_1.it)("多行签名 + 返回类型", () => {
89
+ write("multi.go", `
90
+ package main
91
+
92
+ func Process(
93
+ name string,
94
+ count int,
95
+ ) (string, error) {
96
+ return name, nil
97
+ }
98
+ `);
99
+ const ir = (0, extract_ir_go_1.extractIRGo)(dir);
100
+ const fn = ir.find((f) => f.name === "Process");
101
+ (0, vitest_1.expect)(fn).toBeTruthy();
102
+ (0, vitest_1.expect)(fn?.returnType).toContain("string");
103
+ });
104
+ (0, vitest_1.it)("注释注解 @progmune + 文档标签", () => {
105
+ write("auth.go", `
106
+ package main
107
+
108
+ // @progmune(namespace="auth", pre=["UNAUTHENTICATED"], post=["PASSWORD_VERIFIED"])
109
+ // @purpose 凭证比对
110
+ // @tags auth
111
+ func VerifyPassword(user, password string) bool {
112
+ return password == "secret"
113
+ }
114
+ `);
115
+ const ir = (0, extract_ir_go_1.extractIRGo)(dir);
116
+ const fn = ir.find((f) => f.name === "VerifyPassword");
117
+ (0, vitest_1.expect)(fn?.protocol?.pre_states).toEqual(["UNAUTHENTICATED"]);
118
+ (0, vitest_1.expect)(fn?.protocol?.post_states).toEqual(["PASSWORD_VERIFIED"]);
119
+ (0, vitest_1.expect)(fn?.protocol?.namespace).toBe("auth");
120
+ (0, vitest_1.expect)(fn?.purpose).toBe("凭证比对");
121
+ (0, vitest_1.expect)(fn?.tags).toContain("auth");
122
+ (0, vitest_1.expect)(fn?.exported).toBe(true);
123
+ });
124
+ (0, vitest_1.it)("接口方法声明(无函数体)不提取", () => {
125
+ write("iface.go", `
126
+ package main
127
+
128
+ type Store interface {
129
+ Save(u User) error
130
+ }
131
+ `);
132
+ const ir = (0, extract_ir_go_1.extractIRGo)(dir);
133
+ (0, vitest_1.expect)(ir.some((f) => f.name === "Save")).toBe(false);
134
+ });
135
+ (0, vitest_1.it)("字符串/注释里的伪调用不提取", () => {
136
+ write("str.go", `
137
+ package main
138
+
139
+ func tricky() {
140
+ s := "notAFunction() inside string"
141
+ // notAFunction() in comment
142
+ realCall()
143
+ }
144
+
145
+ func realCall() {}
146
+ `);
147
+ const ir = (0, extract_ir_go_1.extractIRGo)(dir);
148
+ const fn = ir.find((f) => f.name === "tricky");
149
+ (0, vitest_1.expect)(fn?.calls).toContain("realCall");
150
+ (0, vitest_1.expect)(fn?.calls).not.toContain("notAFunction");
151
+ });
152
+ (0, vitest_1.it)("非生产表面过滤:vendor/testdata/测试文件跳过", () => {
153
+ write("main.go", `package main\nfunc Keep() {}\n`);
154
+ write("vendor/dep.go", `package dep\nfunc SkipVendor() {}\n`);
155
+ write("testdata/sample.go", `package sample\nfunc SkipTestdata() {}\n`);
156
+ write("x_test.go", `package main\nfunc SkipTest() {}\n`);
157
+ const ir = (0, extract_ir_go_1.extractIRGo)(dir);
158
+ (0, vitest_1.expect)(ir.map((f) => f.name)).toEqual(["Keep"]);
159
+ });
160
+ (0, vitest_1.it)("Go 关键字不提取为调用(for/if/go/defer 等)", () => {
161
+ write("ctrl.go", `
162
+ package main
163
+
164
+ func run() {
165
+ for i := 0; i < 3; i++ {
166
+ go step(i)
167
+ defer cleanup()
168
+ }
169
+ }
170
+
171
+ func step(i int) {}
172
+ func cleanup() {}
173
+ `);
174
+ const ir = (0, extract_ir_go_1.extractIRGo)(dir);
175
+ const fn = ir.find((f) => f.name === "run");
176
+ (0, vitest_1.expect)(fn?.calls).toEqual(vitest_1.expect.arrayContaining(["step", "cleanup"]));
177
+ (0, vitest_1.expect)(fn?.calls).not.toEqual(vitest_1.expect.arrayContaining(["for", "go", "defer", "if"]));
178
+ });
179
+ });
@@ -54,6 +54,7 @@ const path = __importStar(require("path"));
54
54
  const extract_ir_1 = require("./extract-ir");
55
55
  const extract_ir_python_1 = require("./extract-ir-python");
56
56
  const extract_ir_c_1 = require("./extract-ir-c");
57
+ const extract_ir_go_1 = require("./extract-ir-go");
57
58
  const SKIP_DIRS = new Set([
58
59
  "node_modules", "dist", "build", ".git", ".progmune_corpus",
59
60
  "__pycache__", "venv", ".venv",
@@ -105,6 +106,11 @@ exports.LANGUAGE_EXTRACTORS = [
105
106
  detect: (p) => hasSourceFiles(p, new Set([".c", ".h"])),
106
107
  extract: (p) => (0, extract_ir_c_1.extractIRC)(p),
107
108
  },
109
+ {
110
+ language: "go",
111
+ detect: (p) => hasSourceFiles(p, new Set([".go"])),
112
+ extract: (p) => (0, extract_ir_go_1.extractIRGo)(p),
113
+ },
108
114
  ];
109
115
  /** 项目检测到的语言列表(审计/标签用)。 */
110
116
  function detectLanguages(projectRoot) {