progmune-runtime 3.7.3 → 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 = "3.7.2";
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)();
@@ -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>
@@ -508,7 +508,7 @@ AI 生成: create_session(...) ← 当前状态 UNAUTHENTICATED
508
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>
509
509
  <tr><td><strong>真实应用验证</strong>(PyGoat)</td><td>OWASP 故意脆弱 Django 应用、232 条检测逐条人工核实</td><td>标记精确率 <strong>100%</strong>(67 真阳性 / 0 误报);覆盖 14 个漏洞类别</td></tr>
510
510
  <tr><td><strong>良构应用</strong>(django/fastapi realworld、django-unicorn)</td><td>176 条检测</td><td>0 条误报真阳性;3 条框架内部边界 FP(已定性归档)</td></tr>
511
- <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>
512
512
  <tr><td><strong>PLSB v1.0</strong></td><td>13 类协议安全弱点、39 个手工验证缺陷案例</td><td>覆盖 <strong>13/13 全类别</strong>(业界唯一)</td></tr>
513
513
  </table>
514
514
  <div class="plain">
@@ -537,7 +537,7 @@ AI 生成: create_session(...) ← 当前状态 UNAUTHENTICATED
537
537
  <tr><th>能力</th><th>状态</th><th>说明</th></tr>
538
538
  <tr><td>TypeScript / JavaScript</td><td><span style="color:var(--green);font-weight:700">✅ 生产</span></td><td>盲测 100 项目:精确率 100%、召回率 98.5%</td></tr>
539
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>
540
- <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>
541
541
  <tr><td>Go / Java</td><td><span style="color:var(--ink-3)">❌ 未支持</span></td><td>路线图内</td></tr>
542
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>
543
543
  </table>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.3",
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/",