progmune-runtime 3.7.2 → 3.7.4

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.
@@ -0,0 +1,400 @@
1
+ "use strict";
2
+ /**
3
+ * C IR extractor tests — fixture-string-based parser tests via parseCSource
4
+ * (FS I/O limited to one mkdtemp integration case, per repo convention).
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ const vitest_1 = require("vitest");
41
+ const fs = __importStar(require("fs"));
42
+ const os = __importStar(require("os"));
43
+ const path = __importStar(require("path"));
44
+ const extract_ir_c_1 = require("./extract-ir-c");
45
+ const PROJECT = "/proj";
46
+ /** Parse a string as /proj/x.c → file "x.c". */
47
+ function parse(src, file = "x.c") {
48
+ return (0, extract_ir_c_1.parseCSource)(src, path.join(PROJECT, file), PROJECT);
49
+ }
50
+ function fn(ir, name) {
51
+ const f = ir.find((x) => x.name === name);
52
+ (0, vitest_1.expect)(f, `expected function ${name}`).toBeDefined();
53
+ return f;
54
+ }
55
+ (0, vitest_1.describe)("extract-ir-c", () => {
56
+ (0, vitest_1.it)("基础签名 + 调用列表(auth_flow fixture 内容)", () => {
57
+ const ir = parse(`
58
+ int authenticate(const char* user, const char* pass) {
59
+ if (!verify_password(user, pass)) return 0;
60
+ char* token = generate_jwt(user);
61
+ session_t* sess = create_session(token);
62
+ return sess ? 1 : 0;
63
+ }
64
+ void do_logout(session_t* sess) {
65
+ logout(sess);
66
+ }
67
+ `);
68
+ (0, vitest_1.expect)(ir.map((f) => f.name)).toEqual(["authenticate", "do_logout"]);
69
+ const auth = fn(ir, "authenticate");
70
+ (0, vitest_1.expect)(auth.params).toEqual([
71
+ { name: "user", type: "const char*" },
72
+ { name: "pass", type: "const char*" },
73
+ ]);
74
+ (0, vitest_1.expect)(auth.returnType).toBe("int");
75
+ (0, vitest_1.expect)(auth.calls).toEqual(["verify_password", "generate_jwt", "create_session"]);
76
+ (0, vitest_1.expect)(auth.exported).toBe(true);
77
+ (0, vitest_1.expect)(auth.external).toBe(false); // isProjectFn 契约
78
+ (0, vitest_1.expect)(auth.file).toBe("x.c");
79
+ (0, vitest_1.expect)(auth.tags).toEqual(["c"]);
80
+ const logout = fn(ir, "do_logout");
81
+ (0, vitest_1.expect)(logout.params).toEqual([{ name: "sess", type: "session_t*" }]);
82
+ (0, vitest_1.expect)(logout.returnType).toBe("void");
83
+ (0, vitest_1.expect)(logout.calls).toEqual(["logout"]);
84
+ (0, vitest_1.expect)(logout.outputs).toEqual([]);
85
+ });
86
+ (0, vitest_1.it)("static 函数 → exported=false,仍被提取", () => {
87
+ const ir = parse(`static int helper(int x) { return x; }`);
88
+ const h = fn(ir, "helper");
89
+ (0, vitest_1.expect)(h.exported).toBe(false);
90
+ (0, vitest_1.expect)(h.params).toEqual([{ name: "x", type: "int" }]);
91
+ });
92
+ (0, vitest_1.it)("多行签名 + 无空格星号返回类型", () => {
93
+ const ir = parse(`
94
+ int
95
+ compute(const char* a) {
96
+ return strlen(a);
97
+ }
98
+ static const char*
99
+ get_name(void) {
100
+ return "x";
101
+ }
102
+ `);
103
+ const c = fn(ir, "compute");
104
+ (0, vitest_1.expect)(c.returnType).toBe("int");
105
+ (0, vitest_1.expect)(c.params).toEqual([{ name: "a", type: "const char*" }]);
106
+ (0, vitest_1.expect)(c.calls).toEqual(["strlen"]);
107
+ const g = fn(ir, "get_name");
108
+ (0, vitest_1.expect)(g.returnType).toBe("const char*");
109
+ (0, vitest_1.expect)(g.exported).toBe(false);
110
+ (0, vitest_1.expect)(g.params).toEqual([]);
111
+ });
112
+ (0, vitest_1.it)("注释与字符串内的花括号不腐蚀括号计数", () => {
113
+ const ir = parse(`
114
+ void f(void) {
115
+ /* { */
116
+ // }
117
+ const char* s = "}";
118
+ g();
119
+ }
120
+ void g2(void) { h(); }
121
+ `);
122
+ (0, vitest_1.expect)(ir.map((x) => x.name)).toEqual(["f", "g2"]);
123
+ (0, vitest_1.expect)(fn(ir, "f").calls).toEqual(["g"]);
124
+ (0, vitest_1.expect)(fn(ir, "g2").calls).toEqual(["h"]);
125
+ });
126
+ (0, vitest_1.it)("字符串内容不产生调用", () => {
127
+ const ir = parse(`void f(void) { const char* s = "foo("; g(); }`);
128
+ (0, vitest_1.expect)(fn(ir, "f").calls).toEqual(["g"]);
129
+ });
130
+ (0, vitest_1.it)("块注释 @progmune 注解", () => {
131
+ const ir = parse(`
132
+ /* @progmune(namespace="auth", pre=["UNAUTHENTICATED"], post=["PASSWORD_VERIFIED"]) */
133
+ void verify(const char* u, const char* p) { check(u, p); }
134
+ `);
135
+ const v = fn(ir, "verify");
136
+ (0, vitest_1.expect)(v.protocol).toEqual({
137
+ namespace: "auth",
138
+ pre_states: ["UNAUTHENTICATED"],
139
+ post_states: ["PASSWORD_VERIFIED"],
140
+ });
141
+ (0, vitest_1.expect)(v.calls).toEqual(["check"]);
142
+ });
143
+ (0, vitest_1.it)("多行注解 + 全部文档标签", () => {
144
+ const ir = parse(`
145
+ /*
146
+ * @progmune(namespace="auth", pre=["A"], post=["B"], invalidate=["C"])
147
+ * @purpose verify user
148
+ * @description verifies credentials
149
+ * @tags auth, security
150
+ * @requires P1, P2
151
+ * @produces T1
152
+ * @useWhen login; recovery
153
+ * @inputs user, pass
154
+ * @outputs token
155
+ */
156
+ int auth(const char* user, const char* pass) { return 1; }
157
+ `);
158
+ const a = fn(ir, "auth");
159
+ (0, vitest_1.expect)(a.protocol).toEqual({
160
+ namespace: "auth",
161
+ pre_states: ["A"],
162
+ post_states: ["B"],
163
+ invalidate: ["C"],
164
+ });
165
+ (0, vitest_1.expect)(a.purpose).toBe("verify user");
166
+ (0, vitest_1.expect)(a.description).toBe("verifies credentials");
167
+ (0, vitest_1.expect)(a.tags).toEqual(["auth", "security"]);
168
+ (0, vitest_1.expect)(a.requires).toEqual(["P1", "P2"]);
169
+ (0, vitest_1.expect)(a.produces).toEqual(["T1"]);
170
+ (0, vitest_1.expect)(a.useWhen).toEqual(["login", "recovery"]);
171
+ (0, vitest_1.expect)(a.inputs).toEqual(["user", "pass"]);
172
+ (0, vitest_1.expect)(a.outputs).toEqual(["token"]);
173
+ });
174
+ (0, vitest_1.it)("// @progmune 单行变体", () => {
175
+ const ir = parse(`
176
+ // @progmune(namespace="file", pre=["OPEN"], post=["CLOSED"])
177
+ void open_file(void) { }
178
+ `);
179
+ (0, vitest_1.expect)(fn(ir, "open_file").protocol).toEqual({
180
+ namespace: "file",
181
+ pre_states: ["OPEN"],
182
+ post_states: ["CLOSED"],
183
+ });
184
+ });
185
+ (0, vitest_1.it)("注解与函数之间允许空行", () => {
186
+ const ir = parse(`
187
+ /* @progmune(namespace="auth", pre=["A"], post=["B"]) */
188
+
189
+ void f(void) {}
190
+ `);
191
+ (0, vitest_1.expect)(fn(ir, "f").protocol?.namespace).toBe("auth");
192
+ });
193
+ (0, vitest_1.it)("只有文档标签、无 @progmune → protocol undefined", () => {
194
+ const ir = parse(`
195
+ /**
196
+ * @purpose read config
197
+ * @tags io
198
+ */
199
+ void read_conf(void) { }
200
+ `);
201
+ const r = fn(ir, "read_conf");
202
+ (0, vitest_1.expect)(r.purpose).toBe("read config");
203
+ (0, vitest_1.expect)(r.tags).toEqual(["io"]);
204
+ (0, vitest_1.expect)(r.protocol).toBeUndefined();
205
+ });
206
+ (0, vitest_1.it)("纯文件头注释不挂载到首个函数", () => {
207
+ const ir = parse(`
208
+ /* module overview — plain description */
209
+ int f(void) { return 0; }
210
+ `);
211
+ const f = fn(ir, "f");
212
+ (0, vitest_1.expect)(f.purpose).toBe("");
213
+ (0, vitest_1.expect)(f.description).toBe("");
214
+ (0, vitest_1.expect)(f.protocol).toBeUndefined();
215
+ });
216
+ (0, vitest_1.it)("参数边界:数组/多维/函数指针/变参/裸 void/多词类型", () => {
217
+ const ir = parse(`
218
+ void a(char buf[256]) {}
219
+ void b(int m[2][3]) {}
220
+ void c(void (*cb)(int)) {}
221
+ void d(...) {}
222
+ void e(void) {}
223
+ void g(unsigned long long n) {}
224
+ `);
225
+ (0, vitest_1.expect)(fn(ir, "a").params).toEqual([{ name: "buf", type: "char" }]);
226
+ (0, vitest_1.expect)(fn(ir, "b").params).toEqual([{ name: "m", type: "int" }]);
227
+ (0, vitest_1.expect)(fn(ir, "c").params).toEqual([{ name: "cb", type: "void (*cb)(int)" }]);
228
+ (0, vitest_1.expect)(fn(ir, "d").params).toEqual([{ name: "...", type: "..." }]);
229
+ (0, vitest_1.expect)(fn(ir, "e").params).toEqual([]);
230
+ (0, vitest_1.expect)(fn(ir, "g").params).toEqual([{ name: "n", type: "unsigned long long" }]);
231
+ });
232
+ (0, vitest_1.it)("成员调用取 ->/. 之后的调用名(函数指针分发仍静态不可见)", () => {
233
+ const ir = parse(`void close_conn(conn_t* cf) { cf->close_one(); cf->next->close_two(); obj.method(x); }`);
234
+ (0, vitest_1.expect)(fn(ir, "close_conn").calls).toEqual(["close_one", "close_two", "method"]);
235
+ });
236
+ (0, vitest_1.it)("自调用被排除;重复调用保留(状态机需要重复语义)", () => {
237
+ const ir = parse(`void f(void) { f(); g(); g(); h(); }`);
238
+ (0, vitest_1.expect)(fn(ir, "f").calls).toEqual(["g", "g", "h"]);
239
+ });
240
+ (0, vitest_1.it)("struct 定义体被跳过(单行 + 多行)", () => {
241
+ const ir = parse(`
242
+ typedef struct { int x; } Foo;
243
+ void after_struct(void) { g(); }
244
+ typedef struct {
245
+ int x;
246
+ } Bar;
247
+ void after_struct2(void) { h(); }
248
+ `);
249
+ (0, vitest_1.expect)(ir.map((x) => x.name)).toEqual(["after_struct", "after_struct2"]);
250
+ (0, vitest_1.expect)(fn(ir, "after_struct").calls).toEqual(["g"]);
251
+ (0, vitest_1.expect)(fn(ir, "after_struct2").calls).toEqual(["h"]);
252
+ });
253
+ (0, vitest_1.it)("goto 合成 goto_<label> 调用", () => {
254
+ const ir = parse(`void f(void) { goto cleanup; g(); cleanup: ; }`);
255
+ (0, vitest_1.expect)(fn(ir, "f").calls).toEqual(["g", "goto_cleanup"]);
256
+ });
257
+ (0, vitest_1.it)("函数体内的预处理行整体跳过(花括号与调用均不计数)", () => {
258
+ const ir = parse(`
259
+ void f(void) {
260
+ #define X {
261
+ #undef X
262
+ g();
263
+ }
264
+ `);
265
+ (0, vitest_1.expect)(fn(ir, "f").calls).toEqual(["g"]);
266
+ });
267
+ (0, vitest_1.it)("嵌套块括号正确闭合", () => {
268
+ const ir = parse(`void f(void) { if (x) { g(); } h(); }`);
269
+ (0, vitest_1.expect)(fn(ir, "f").calls).toEqual(["g", "h"]);
270
+ });
271
+ (0, vitest_1.it)("__attribute__ 前缀被剥离,不进返回类型", () => {
272
+ const ir = parse(`
273
+ static __attribute__((unused)) int helper(void) { return 0; }
274
+ void caller(void) { helper(); }
275
+ `);
276
+ const h = fn(ir, "helper");
277
+ (0, vitest_1.expect)(h.returnType).toBe("int");
278
+ (0, vitest_1.expect)(h.exported).toBe(false);
279
+ (0, vitest_1.expect)(fn(ir, "caller").calls).toEqual(["helper"]);
280
+ });
281
+ (0, vitest_1.it)("头文件原型被忽略;static inline 定义被提取", () => {
282
+ const ir = parse(`
283
+ int prototype_only(const char* x);
284
+ static inline int add1(int x) { return x + 1; }
285
+ `, "header.h");
286
+ (0, vitest_1.expect)(ir.map((x) => x.name)).toEqual(["add1"]);
287
+ (0, vitest_1.expect)(fn(ir, "add1").exported).toBe(false);
288
+ (0, vitest_1.expect)(fn(ir, "add1").params).toEqual([{ name: "x", type: "int" }]);
289
+ });
290
+ (0, vitest_1.it)("extractIRC 集成:真实 fixture 内容", () => {
291
+ const authFlow = `
292
+ int authenticate(const char* user, const char* pass) {
293
+ if (!verify_password(user, pass)) return 0;
294
+ char* token = generate_jwt(user);
295
+ session_t* sess = create_session(token);
296
+ return sess ? 1 : 0;
297
+ }
298
+ void do_logout(session_t* sess) {
299
+ logout(sess);
300
+ }
301
+ `;
302
+ const dbHandler = `
303
+ void run_query(const char* host, const char* sql) {
304
+ connect_db(host);
305
+ query_db(sql);
306
+ disconnect_db();
307
+ }
308
+ void run_insert(const char* host, const char* data) {
309
+ connect_db(host);
310
+ query_db(data);
311
+ disconnect_db();
312
+ }
313
+ void verify_and_session(const char* user, const char* pass) {
314
+ verify_password(user, pass);
315
+ generate_jwt(user);
316
+ create_session();
317
+ }
318
+ void auth_and_logout(const char* user, const char* pass) {
319
+ verify_password(user, pass);
320
+ generate_jwt(user);
321
+ create_session();
322
+ logout();
323
+ }
324
+ `;
325
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-c-"));
326
+ try {
327
+ fs.writeFileSync(path.join(dir, "auth_flow.c"), authFlow);
328
+ fs.mkdirSync(path.join(dir, "src"), { recursive: true });
329
+ fs.writeFileSync(path.join(dir, "src", "db_handler.c"), dbHandler);
330
+ const ir = (0, extract_ir_c_1.extractIRC)(dir);
331
+ (0, vitest_1.expect)(ir.map((f) => f.name).sort()).toEqual([
332
+ "auth_and_logout", "authenticate", "do_logout",
333
+ "run_insert", "run_query", "verify_and_session",
334
+ ]);
335
+ const runQuery = fn(ir, "run_query");
336
+ (0, vitest_1.expect)(runQuery.file).toBe(path.join("src", "db_handler.c"));
337
+ (0, vitest_1.expect)(runQuery.calls).toEqual(["connect_db", "query_db", "disconnect_db"]);
338
+ (0, vitest_1.expect)(fn(ir, "auth_and_logout").calls).toEqual(["verify_password", "generate_jwt", "create_session", "logout"]);
339
+ // isProjectFn 契约:全部 external=false + 真实相对路径
340
+ for (const f of ir) {
341
+ (0, vitest_1.expect)(f.external).toBe(false);
342
+ (0, vitest_1.expect)(f.file).toBeTruthy();
343
+ (0, vitest_1.expect)(f.file).not.toBe("(external)");
344
+ }
345
+ }
346
+ finally {
347
+ fs.rmSync(dir, { recursive: true, force: true });
348
+ }
349
+ });
350
+ (0, vitest_1.it)("回归:顶层长标识符调用赋值行不触发指数级回溯(44 字符缓冲 11s → 即时)", () => {
351
+ // libssh authentication.c 真实触发:v2 签名正则的类型 token 循环对
352
+ // `name = ssh_userauth_kbdint_getname(session);` 穷举标识符切分(2^k)
353
+ const t0 = Date.now();
354
+ const ir = parse(`
355
+ int f(void) { return 0; }
356
+ name = ssh_userauth_kbdint_getname(session);
357
+ int g(void) { return 1; }
358
+ `);
359
+ (0, vitest_1.expect)(Date.now() - t0).toBeLessThan(2000);
360
+ (0, vitest_1.expect)(ir.map((x) => x.name)).toEqual(["f", "g"]);
361
+ });
362
+ (0, vitest_1.it)("#if 0 死代码块被剥离:体内不平衡花括号不腐蚀计数", () => {
363
+ const ir = parse(`
364
+ void f(void) {
365
+ #if 0
366
+ void dead(void) { { {
367
+ #endif
368
+ g();
369
+ }
370
+ void g2(void) { h(); }
371
+ `);
372
+ (0, vitest_1.expect)(ir.map((x) => x.name)).toEqual(["f", "g2"]);
373
+ (0, vitest_1.expect)(fn(ir, "f").calls).toEqual(["g"]);
374
+ (0, vitest_1.expect)(fn(ir, "g2").calls).toEqual(["h"]);
375
+ });
376
+ (0, vitest_1.it)("#if 0 死代码块被剥离:顶层死函数不产生幻影函数;嵌套 #if 死区内保持死", () => {
377
+ const ir = parse(`
378
+ #if 0
379
+ void dead_fn(void) { broken {
380
+ #if 1
381
+ void dead_inner(void) { { {
382
+ #endif
383
+ void also_dead(void) { { {
384
+ #endif
385
+ void live_fn(void) { g(); }
386
+ `);
387
+ (0, vitest_1.expect)(ir.map((x) => x.name)).toEqual(["live_fn"]);
388
+ (0, vitest_1.expect)(fn(ir, "live_fn").calls).toEqual(["g"]);
389
+ });
390
+ (0, vitest_1.it)("#if 0 || X 表达式不求值,按活区处理", () => {
391
+ const ir = parse(`
392
+ #if 0 || 1
393
+ void maybe_live(void) { g(); }
394
+ #endif
395
+ void after(void) { h(); }
396
+ `);
397
+ (0, vitest_1.expect)(ir.map((x) => x.name)).toEqual(["maybe_live", "after"]);
398
+ (0, vitest_1.expect)(fn(ir, "maybe_live").calls).toEqual(["g"]);
399
+ });
400
+ });
@@ -53,9 +53,11 @@ const fs = __importStar(require("fs"));
53
53
  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
+ const extract_ir_c_1 = require("./extract-ir-c");
56
57
  const SKIP_DIRS = new Set([
57
58
  "node_modules", "dist", "build", ".git", ".progmune_corpus",
58
59
  "__pycache__", "venv", ".venv",
60
+ "benchmarks", // vendored C 基准仓库(与 extract-ir-c.ts collectCFiles 口径一致)
59
61
  ]);
60
62
  /** 有界递归扫描:项目是否含指定扩展名源文件(首个命中即返回)。 */
61
63
  function hasSourceFiles(projectRoot, exts) {
@@ -98,6 +100,11 @@ exports.LANGUAGE_EXTRACTORS = [
98
100
  detect: (p) => hasSourceFiles(p, new Set([".py"])),
99
101
  extract: (p) => (0, extract_ir_python_1.extractIRPython)(p),
100
102
  },
103
+ {
104
+ language: "c",
105
+ detect: (p) => hasSourceFiles(p, new Set([".c", ".h"])),
106
+ extract: (p) => (0, extract_ir_c_1.extractIRC)(p),
107
+ },
101
108
  ];
102
109
  /** 项目检测到的语言列表(审计/标签用)。 */
103
110
  function detectLanguages(projectRoot) {
@@ -45,6 +45,7 @@ const fs = __importStar(require("fs"));
45
45
  const os = __importStar(require("os"));
46
46
  const path = __importStar(require("path"));
47
47
  const extract_project_ir_1 = require("./extract-project-ir");
48
+ const call_sequence_1 = require("./call-sequence");
48
49
  (0, vitest_1.describe)("extractProjectIR(注册表合并)", () => {
49
50
  (0, vitest_1.it)("合并所有检测到语言的函数", () => {
50
51
  const tsFns = [{ name: "getSession", file: "src/auth.ts" }];
@@ -56,6 +57,18 @@ const extract_project_ir_1 = require("./extract-project-ir");
56
57
  const ir = (0, extract_project_ir_1.extractProjectIR)("/tmp/fake", extractors);
57
58
  (0, vitest_1.expect)(ir).toEqual([...tsFns, ...pyFns]);
58
59
  });
60
+ (0, vitest_1.it)("三语言合并(TS + Python + C)按注册表顺序", () => {
61
+ const tsFns = [{ name: "getSession", file: "src/auth.ts" }];
62
+ const pyFns = [{ name: "verify_password", file: "auth_service.py" }];
63
+ const cFns = [{ name: "authenticate", file: "src/auth_flow.c" }];
64
+ const extractors = [
65
+ { language: "typescript", detect: () => true, extract: () => tsFns },
66
+ { language: "python", detect: () => true, extract: () => pyFns },
67
+ { language: "c", detect: () => true, extract: () => cFns },
68
+ ];
69
+ const ir = (0, extract_project_ir_1.extractProjectIR)("/tmp/fake", extractors);
70
+ (0, vitest_1.expect)(ir).toEqual([...tsFns, ...pyFns, ...cFns]);
71
+ });
59
72
  (0, vitest_1.it)("未检测到的语言不运行提取器", () => {
60
73
  const ran = [];
61
74
  const extractors = [
@@ -74,6 +87,15 @@ const extract_project_ir_1 = require("./extract-project-ir");
74
87
  const ir = (0, extract_project_ir_1.extractProjectIR)("/tmp/fake", extractors);
75
88
  (0, vitest_1.expect)(ir).toEqual([{ name: "f" }]);
76
89
  });
90
+ (0, vitest_1.it)("C 提取器失败被吞,其余语言结果保留", () => {
91
+ vitest_1.vi.spyOn(console, "error").mockImplementation(() => { });
92
+ const extractors = [
93
+ { language: "typescript", detect: () => true, extract: () => [{ name: "f" }] },
94
+ { language: "c", detect: () => true, extract: () => { throw new Error("broken c parse"); } },
95
+ ];
96
+ const ir = (0, extract_project_ir_1.extractProjectIR)("/tmp/fake", extractors);
97
+ (0, vitest_1.expect)(ir).toEqual([{ name: "f" }]);
98
+ });
77
99
  (0, vitest_1.it)("所有检测到的语言都失败时抛错(保留 execute 硬失败语义)", () => {
78
100
  vitest_1.vi.spyOn(console, "error").mockImplementation(() => { });
79
101
  const extractors = [
@@ -113,4 +135,69 @@ const extract_project_ir_1 = require("./extract-project-ir");
113
135
  fs.rmSync(dir, { recursive: true, force: true });
114
136
  }
115
137
  });
138
+ (0, vitest_1.it)("纯 C 项目(.c)被检测", () => {
139
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-lang3-"));
140
+ try {
141
+ fs.writeFileSync(path.join(dir, "main.c"), "int main(void) { return 0; }\n");
142
+ (0, vitest_1.expect)((0, extract_project_ir_1.detectLanguages)(dir)).toEqual(["c"]);
143
+ }
144
+ finally {
145
+ fs.rmSync(dir, { recursive: true, force: true });
146
+ }
147
+ });
148
+ (0, vitest_1.it)("仅头文件(.h)也算 C 项目", () => {
149
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-lang4-"));
150
+ try {
151
+ fs.writeFileSync(path.join(dir, "header.h"), "int add(int a, int b);\n");
152
+ (0, vitest_1.expect)((0, extract_project_ir_1.detectLanguages)(dir)).toEqual(["c"]);
153
+ }
154
+ finally {
155
+ fs.rmSync(dir, { recursive: true, force: true });
156
+ }
157
+ });
158
+ (0, vitest_1.it)("TS + C 混合项目两种语言都被检测", () => {
159
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-lang5-"));
160
+ try {
161
+ fs.writeFileSync(path.join(dir, "main.ts"), "export function f() {}");
162
+ fs.writeFileSync(path.join(dir, "helper.c"), "void helper(void) {}\n");
163
+ (0, vitest_1.expect)((0, extract_project_ir_1.detectLanguages)(dir).sort()).toEqual(["c", "typescript"]);
164
+ }
165
+ finally {
166
+ fs.rmSync(dir, { recursive: true, force: true });
167
+ }
168
+ });
169
+ (0, vitest_1.it)("benchmarks/ 下的 C 源不计入检测(vendored 基准仓库,与 extract 口径一致)", () => {
170
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-lang6-"));
171
+ try {
172
+ fs.mkdirSync(path.join(dir, "benchmarks", "curl"), { recursive: true });
173
+ fs.writeFileSync(path.join(dir, "benchmarks", "curl", "lib.c"), "void f(void) {}\n");
174
+ (0, vitest_1.expect)((0, extract_project_ir_1.detectLanguages)(dir)).toEqual([]);
175
+ }
176
+ finally {
177
+ fs.rmSync(dir, { recursive: true, force: true });
178
+ }
179
+ });
180
+ (0, vitest_1.it)("TS + C 混合项目:真实注册表合并 IR 含两语言函数,词段门控集合为并集", () => {
181
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-mix-"));
182
+ try {
183
+ // ts-morph 需要 tsconfig(无 tsconfig 时 TS 提取抛错、按设计被注册表吞掉)
184
+ fs.writeFileSync(path.join(dir, "tsconfig.json"), JSON.stringify({
185
+ compilerOptions: { target: "es2020", module: "commonjs" },
186
+ include: ["**/*.ts"],
187
+ }));
188
+ fs.writeFileSync(path.join(dir, "auth.ts"), "export function verifySession(t: string) { return t; }\n");
189
+ fs.writeFileSync(path.join(dir, "helper.c"), "void authenticate(void) { verify_password(); generate_jwt(); }\n");
190
+ const ir = (0, extract_project_ir_1.extractProjectIR)(dir);
191
+ const names = ir.map((f) => `${f.file}:${f.name}`).sort();
192
+ (0, vitest_1.expect)(names).toEqual(["auth.ts:verifySession", "helper.c:authenticate"]);
193
+ // 词段匹配门控集合(ssg-bridge projectFunctions)为两语言并集——
194
+ // C 函数名进入集合是设计行为(混合项目 C 侧才参与词段匹配)
195
+ const gate = (0, call_sequence_1.collectProjectFunctionNames)(ir);
196
+ (0, vitest_1.expect)(gate.has("verifySession")).toBe(true);
197
+ (0, vitest_1.expect)(gate.has("authenticate")).toBe(true);
198
+ }
199
+ finally {
200
+ fs.rmSync(dir, { recursive: true, force: true });
201
+ }
202
+ });
116
203
  });
@@ -46,7 +46,11 @@ exports.loadFailures = loadFailures;
46
46
  exports.failureStats = failureStats;
47
47
  exports.formatFailureStats = formatFailureStats;
48
48
  const fs = __importStar(require("fs"));
49
- const CORPUS_DIR = "failure-corpus";
49
+ const path = __importStar(require("path"));
50
+ // 统一写入路径:与 failure-corpus.ts 一致,落在项目级 .progmune_corpus/emitter-failures/
51
+ // (旧路径为仓库根 failure-corpus/,已废弃——避免两套语料并存)
52
+ const CORPUS_DIR = path.resolve(process.env.PROGMUNE_CORPUS_DIR ||
53
+ path.resolve(process.env.PROGMUNE_PROJECT_DIR || process.cwd(), ".progmune_corpus"), "emitter-failures");
50
54
  /** Classify a compile error string into a root cause. */
51
55
  /** Classify a compile error into a root cause category. */
52
56
  /** @requires ERROR_STRING @produces ROOT_CAUSE */
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 = "1.0.0";
23
+ exports.RUNTIME_VERSION = "3.7.4";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
@@ -174,7 +174,7 @@
174
174
  本文档面向两类读者:想快速理解的初学者,与需要专业判断的投资人。</div>
175
175
  <div class="meta">
176
176
  <span>📄 版本 v1.0 · 2026-08</span>
177
- <span>🧪 引擎版本 v3.2.0</span>
177
+ <span>🧪 引擎版本 v3.7.2</span>
178
178
  <span>📊 13/13 安全类别覆盖</span>
179
179
  <span>🔓 开源 MIT</span>
180
180
  <span>📦 npm install progmune-runtime</span>
@@ -346,7 +346,7 @@
346
346
  <div class="bar"><span class="bl">人工审查能力</span><div class="bt"><div class="bf" style="width:10%"></div></div><span class="bv">1x</span></div>
347
347
 
348
348
  <h3>3.2 问题本质:AI 写代码的原理决定了它"看不见"错误</h3>
349
- <p>大语言模型生成代码的方式是<strong>统计预测</strong>——根据训练数据猜"下一个最可能的词"。这一观点的代表性论述来自 Subbarao Kambhampati 等的立场论文《Stop Anthropomorphizing Intermediate Tokens as Reasoning/Thinking Traces!》(arXiv:2505.22285)及其 ICML 2026 关于 verifier 的主题演讲:LLM 输出的"逐步推理"是统计表演,中间 token 与真实推理之间没有可靠的因果对应。</p>
349
+ <p>大语言模型生成代码的方式是<strong>统计预测</strong>——根据训练数据猜"下一个最可能的词"。这一观点的代表性论述来自 Subbarao Kambhampati 等的立场论文《Stop Anthropomorphizing Intermediate Tokens as Reasoning/Thinking Traces!》(arXiv:2504.09762)及其 ICML 2026 关于 verifier 的主题演讲:LLM 输出的"逐步推理"是统计表演,中间 token 与真实推理之间没有可靠的因果对应。</p>
350
350
  <p>这意味着:AI 写出"看似合理的代码"和"实际正确的代码"是两件事。它会一本正经地调用不存在的函数、跳过关键的认证步骤——<strong>而且完全静默,不报错</strong>。</p>
351
351
 
352
352
  <h3>3.3 现有方案为什么不够</h3>
@@ -449,7 +449,7 @@ IR: { name: "createOrder", params: [{name:"userId", type:"number"}...],
449
449
 
450
450
  <h3>5.2 协议状态机:把"顺序规则"变成可执行检查</h3>
451
451
  <p>每个关键协议被建模为一个状态机。以登录为例:</p>
452
- <pre><code>规则(protocols.json,共 148 条,覆盖 21 个命名空间):
452
+ <pre><code>规则(protocols.json,共 148 条,覆盖 27 个命名空间):
453
453
  verify_password: UNAUTHENTICATED → PASSWORD_VERIFIED
454
454
  generate_jwt: PASSWORD_VERIFIED → TOKEN_ISSUED
455
455
  create_session: TOKEN_ISSUED → SESSION_ACTIVE
@@ -505,9 +505,10 @@ AI 生成: create_session(...) ← 当前状态 UNAUTHENTICATED
505
505
  <tr><th>基准</th><th>方法</th><th>结果</th></tr>
506
506
  <tr><td><strong>盲测基准</strong>(TypeScript,100 项目)</td><td>90 个合成风格变体 + 10 个模型变体项目、795 条 gold finding、严格定位匹配</td><td>精确率 <strong>100%</strong> · 召回率 <strong>98.5%</strong>(有效口径 100%)· 事实性误报 <strong>0</strong></td></tr>
507
507
  <tr><td><strong>盲测基准</strong>(Python,90 项目)</td><td>90 个合成风格变体、729 条 gold finding</td><td>精确率 <strong>100%</strong> · 召回率 <strong>100%</strong> · 事实性误报 <strong>0</strong></td></tr>
508
+ <tr><td><strong>协议盲测</strong>(Python v1.2,38 项目)</td><td>8 违规类型 × 5 结构/命名风格、66 条可测金标、生产 SSG 桥接校验器(P4.6 跨函数传播)</td><td>召回 <strong>97%</strong> · 精确率 <strong>100%</strong> · 误报 <strong>0</strong>(2 漏检为注解依赖前置,已单列)</td></tr>
508
509
  <tr><td><strong>真实应用验证</strong>(PyGoat)</td><td>OWASP 故意脆弱 Django 应用、232 条检测逐条人工核实</td><td>标记精确率 <strong>100%</strong>(67 真阳性 / 0 误报);覆盖 14 个漏洞类别</td></tr>
509
510
  <tr><td><strong>良构应用</strong>(django/fastapi realworld、django-unicorn)</td><td>176 条检测</td><td>0 条误报真阳性;3 条框架内部边界 FP(已定性归档)</td></tr>
510
- <tr><td><strong>黄金基准</strong>(C 语言)</td><td>curl/libssh/nginx/openssl 等真实 CVE 案例</td><td>F1 16.5% —— <strong>研究阶段</strong>,诚实披露</td></tr>
511
+ <tr><td><strong>黄金基准</strong>(C 语言)</td><td>curl/libssh/nginx/openssl 等真实 CVE 案例;3.7.4 起 IR 提取接入注册表</td><td>应用级协议验证金标 v2:<strong>F1 95.7%</strong>(召回 100%);旧正则口径 F1=16.5% 为 TLS 级历史基线 —— <strong>研究阶段</strong>,诚实披露</td></tr>
511
512
  <tr><td><strong>PLSB v1.0</strong></td><td>13 类协议安全弱点、39 个手工验证缺陷案例</td><td>覆盖 <strong>13/13 全类别</strong>(业界唯一)</td></tr>
512
513
  </table>
513
514
  <div class="plain">
@@ -526,6 +527,7 @@ AI 生成: create_session(...) ← 当前状态 UNAUTHENTICATED
526
527
  <tr><td><strong>SDK</strong></td><td>一行调用:<code>verify(file) → BLOCK/WARN/ALLOW</code></td><td>✅ 可用</td></tr>
527
528
  <tr><td><strong>CLI</strong></td><td><code>npm run trust</code>、<code>npm run governance</code>——命令行信任检查与治理报告(终端/JSON/Markdown/HTML 四种格式)</td><td>✅ 可用</td></tr>
528
529
  <tr><td><strong>GitHub Action</strong></td><td>PR 合并前自动门禁</td><td>✅ 可用</td></tr>
530
+ <tr><td><strong>社区自动回复</strong></td><td>公众号(wechat-bot)与 WhatsApp(whatsapp-bot)双渠道关键词自动回复,「群」指令发送社区二维码合成图</td><td>✅ 上线(3.7.2)</td></tr>
529
531
  <tr><td><strong>Trust API</strong></td><td><code>POST /trust/check</code> 机器间接口</td><td>✅ 可用</td></tr>
530
532
  <tr><td><strong>SaaS 仪表盘</strong></td><td>多项目可视化管理</td><td>🔲 规划中(等 PoC 验证后)</td></tr>
531
533
  </table>
@@ -535,7 +537,7 @@ AI 生成: create_session(...) ← 当前状态 UNAUTHENTICATED
535
537
  <tr><th>能力</th><th>状态</th><th>说明</th></tr>
536
538
  <tr><td>TypeScript / JavaScript</td><td><span style="color:var(--green);font-weight:700">✅ 生产</span></td><td>盲测 100 项目:精确率 100%、召回率 98.5%</td></tr>
537
539
  <tr><td>Python</td><td><span style="color:var(--green);font-weight:700">✅ 生产</span></td><td>盲测 90 项目:精确率/召回率双 100%;15 条源码级检测规则;PyGoat 真实验证 67 TP / 0 FP</td></tr>
538
- <tr><td>C</td><td><span style="color:var(--amber);font-weight:700">⚠️ 研究</span></td><td>F1 16.5%,瓶颈已定位(规则覆盖),L3 实验已终止</td></tr>
540
+ <tr><td>C</td><td><span style="color:var(--amber);font-weight:700">⚠️ 研究</span></td><td>3.7.4 起 IR 提取 + 应用级协议状态机验证(金标 v2:P=91.7%/R=100%/F1=95.7%);TLS 级仍无覆盖(旧正则口径 F1=16.5% 为历史基线),L3/L4 结论不变</td></tr>
539
541
  <tr><td>Go / Java</td><td><span style="color:var(--ink-3)">❌ 未支持</span></td><td>路线图内</td></tr>
540
542
  <tr><td>框架适配</td><td><span style="color:var(--amber);font-weight:700">2/13</span></td><td>Express ✅、tRPC ✅、NestJS 部分;Next.js 版本感知已实现;Django/FastAPI 经 Python 源码级检测间接覆盖</td></tr>
541
543
  </table>
@@ -543,7 +545,7 @@ AI 生成: create_session(...) ← 当前状态 UNAUTHENTICATED
543
545
  <h3>6.3 知识库规模</h3>
544
546
  <div class="stats">
545
547
  <div class="stat"><div class="n">148</div><div class="l">协议规则</div></div>
546
- <div class="stat"><div class="n">21</div><div class="l">协议命名空间全覆盖</div></div>
548
+ <div class="stat"><div class="n">27</div><div class="l">协议命名空间全覆盖</div></div>
547
549
  <div class="stat"><div class="n">22+26+15</div><div class="l">检测器 + 防护 + 源码级规则</div></div>
548
550
  <div class="stat"><div class="n">39</div><div class="l">黄金缺陷案例(人工验证)</div></div>
549
551
  <div class="stat"><div class="n">190</div><div class="l">盲测项目(TS 100 + Python 90)</div></div>
@@ -693,7 +695,7 @@ AI 生成: create_session(...) ← 当前状态 UNAUTHENTICATED
693
695
  <li><strong>真问题 + 真窗口</strong>:76% 企业已用 AI 编码,验证层是明确的基础设施缺口,且没有巨头占位</li>
694
696
  <li><strong>差异化足够深</strong>:协议顺序验证是传统 SAST 的系统性盲区——这是"品类级"差异,不是"功能级"差异</li>
695
697
  <li><strong>数据飞轮成立</strong>:每次拦截都沉淀为抗体,使用量即壁垒(与 Salesforce 的网络效应同构)</li>
696
- <li><strong>开源已验证</strong>:19 个工具、148 条规则、85.2% F1、真实项目案例(PrintLab 从 BLOCKED 到 APPROVED 的完整过程可复现)</li>
698
+ <li><strong>开源已验证</strong>:MCP 工具集成、148 条规则、TS 盲测 Recall 98.5% / Precision 100%(0 FP)、真实项目案例(PrintLab 从 BLOCKED 到 APPROVED 的完整过程可复现)</li>
697
699
  </ol>
698
700
 
699
701
  <h3>为什么需要谨慎</h3>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.2",
3
+ "version": "3.7.4",
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/",