progmune-runtime 3.7.46 → 3.7.48

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,105 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.48] — 2026-09-22
4
+
5
+ ### IR 提取失败必须发声(OOM 杀后的「0 违规假干净」根治)
6
+
7
+ - **事故**:skyvern E1 重扫时 python3 提取器被 OOM 杀(SIGKILL),ir.json 被写成 2 字节空数组——引擎带着空 IR 照样输出「0 违规、85 分 APPROVED」,结果自洽、无任何迹象表明它是废票(静默降级比 R8 陈旧产物更阴)
8
+ - **修复**(`src/trust/engine.ts` + `types.ts`):
9
+ - 提取异常不再静默吞掉——异常信息记入 `extractionWarning`
10
+ - **提取后验尸**:ir.json 存在但函数数=0 且项目确有源码 ⇒ 记失败并 console.warn
11
+ - TrustDecision 新增 `overall.extractionWarning` 字段;失败时 `coverageConfidence` 强制降 LOW,摘要明示「本次扫描是废票,不得据此得出干净结论」
12
+ - **E2E 验证**:空 ir.json + 1 个源文件 → warning 触发 / confidence LOW / 摘要正确
13
+ - 回归:engine 21 + ir-staleness 12 + structural 165 全绿;build ✓
14
+ - 后续:python3 提取分块(skyvern 级项目单进程内存峰值不可控,按目录分批提取、引擎侧合并)——机器资源恢复后按四门验收
15
+
16
+ ## [3.7.47] — 2026-09-21
17
+
18
+ ### 类方法的真实调用必须进 IR(状态机在 OO 代码上没有输入)
19
+
20
+ **一句话**:主 IR 循环的类方法分支**只调了 `computeMarkerCalls`(产 `__progmune_*` 语义
21
+ 标记),从来没调过 `extractDirectCalls`(产真实调用名)**;而函数声明 / 箭头 / 包装箭头
22
+ 三个分支都是两行齐全。后果是类方法的 `calls` 里永远只有标记、没有真实调用 ⇒ **协议状态
23
+ 机在 OO 代码上拿不到输入**。
24
+
25
+ 这不是新引入的回归。Claude 补充的历史事实:修 A1 时类方法提取循环里写死的就是
26
+ `calls: []`,`extractDirectCalls` 只对函数声明和箭头函数调用过,类方法从未接上。A1 只是
27
+ 给方法条目挂标记时动过这个数组,基础的调用提取从来没补。**自该功能诞生之日起就空转**,
28
+ taintpath 十一族语料全是函数载体,从来没有东西逼它现形。
29
+
30
+ **顺带解释了三件悬案**:真实语料 `protocol=0`、引擎自扫「0 误报」、NestJS 语料过分干净
31
+ —— 裁判一直没拿到证据。
32
+
33
+ **修复**:类方法分支补上 `extractDirectCalls(m, mText)`(签名形参加 `MethodDeclaration`,
34
+ 遍历逻辑无需改动),顺序与既有三个载体一致:先真实调用,再追加语义标记。
35
+
36
+ | 探针形态(18 条) | 修复前 | 修复后 |
37
+ |---|---|---|
38
+ | 类方法(同步 / async / this 成员 / 装饰器 / 构造函数注入) | 全空 | ✓ 全部提出 |
39
+ | static / private / 抽象类实现方法 / `this.a.b()` 链式 / 计算属性名 | 全空 | ✓ 全部提出 |
40
+ | 函数声明 / 箭头(基线对照) | ✓ | ✓ 不变 |
41
+
42
+ **三条不修**(探针确认后明确登记,避免下一轮重踩):
43
+
44
+ 1. **getter / setter / 构造函数体 / 对象字面量方法 / 类表达式** —— 这五种不是「calls 空」,
45
+ 而是**条目压根不在 IR 里**(载体未收集)。属另一类缺陷,风险量级不同(新增条目 = 新增
46
+ 检测面),不能与本轮混在一起,否则反向验证无法归因。
47
+ 2. **嵌套箭头回调里的调用** —— `extractDirectCalls` 遇嵌套函数即 `traversal.skip()`。实测
48
+ 函数声明 / 箭头 / 类方法三者口径**完全一致**(都只提得到 `setTimeout`),是既有设计。
49
+ 3. **计算属性名的名字解算** —— `C.[K]` 名字不可解是另一回事,不影响 calls。
50
+
51
+ ### FP 观测池:把「裁判拿到证据」量化出来
52
+
53
+ 修复后重扫(`fp-pool-scan.ts`,4 切片 / 197 文件):
54
+
55
+ | 读数 | 修复前 | 修复后 |
56
+ |---|---|---|
57
+ | 空 calls 函数 | 140 / 197(**71%**) | 23 / 197(**12%**) |
58
+ | 其中类方法空转 | 126 | 9 |
59
+ | NestJS 切片 | 59 / 61 空 | 3 / 61 空 |
60
+ | Express 切片 | 76 / 113 空 | 15 / 113 空 |
61
+
62
+ **代价也量出来了**:项目级 safeguard 19 → 21,但 **perFunction 层面新增 18 条** —— 类方法
63
+ 第一次有了 calls,规则第一次看见这些序列。逐条判定:**FP 15 / 弱真 3 / 真漏洞 0**。四条
64
+ 规则侧校准线索已登记在 `fp-pool.json`(迁移与种子脚本应豁免、TypeORM query builder 链被
65
+ Input Validation 误判、内部工具方法不应被要求 Authorization、`validate` 已存在却仍报缺校验
66
+ ⇒ 是词表不认 class-validator)。
67
+
68
+ 上一轮 7 条人工判定里那条「无法判定」的 `UserController.login`(Token Security)**本轮变
69
+ 成有效票**:有了 calls 才看得出它是标准 `jwt.sign` 签发 ⇒ FP。
70
+
71
+ ### 验收
72
+
73
+ | 门 | 结果 |
74
+ |---|---|
75
+ | taintpath 闸门 | **160/160** |
76
+ | fr-007 / fr-016 | pre 5→post 0 / pre 7→post 0 **均维持** |
77
+ | 定向用例 | **241 passed / 6 文件**(新增 E1 组 14 条:10 正 2 正对照 2 负对照) |
78
+ | TS 盲测 | 111 项目 / perFunction **3201 条**:**LOST 0 / ADDED 0**,汇总完全一致 |
79
+ | 反向验证 | 三刀,残留 0(见下) |
80
+ | tsc | 零错误 |
81
+
82
+ **反向验证三刀**(`/tmp/rev_e1.py(原名 rev_c4k.py)`):
83
+
84
+ | 刀 | 退回的旧行为 | 转红 |
85
+ |---|---|---|
86
+ | ① 类方法只留标记 | `const mCalls = computeMarkerCalls(...)` | **12 条**:正例 ①–⑩ + 负对照 ①② |
87
+ | ② 类方法不追加标记 | 摘掉 `mCalls.push(...computeMarkerCalls(...))` | 0 条(marker 由**调用方侧**补上) |
88
+ | ③ 同上(闸门视角) | 同 ② | 闸门仍 160/160 |
89
+
90
+ 刀①归因**干净**:转红的 12 条全在类方法组,正对照 ⑪⑫(函数声明 / 箭头)仍绿。
91
+ 刀②③顺带证明两件事:marker 链路有调用方侧兜底(不是单点);**taintpath 语料里根本没有
92
+ 类方法 ⇒ 闸门对本轮是空过**,真正的验收在 fp-pool 与定向用例。
93
+
94
+ ### 方法学
95
+
96
+ - **R22 第三次应验**:负对照·② 一开始写错期望(以为类方法体内有 sink 就会有 marker),
97
+ 跑探针才确认 —— marker 只在**调用方把污点传进来**时才注入到被调用方。写 expect 之前
98
+ 先把判据抄进探针注释,这条规矩已经是第三次救我。
99
+ - **R20 的适用边界**(Claude 指出):那三问(pre 有无校验 / 污点根是否在切片内 / fix 是否
100
+ 新增校验)只适用于**修复回归语料**,**别误用到 FP 观测池**——池的标准是「真实 + HTTP
101
+ 入口 + 文件操作」,不要求 CVE。两边管的方向不同(召回 vs 精度),标准本来就不同。
102
+
3
103
  ## [3.7.46] — 2026-09-21
4
104
 
5
105
  ### C4j:载体收口的最后一批 + 一处既有缺陷的回归门
@@ -1898,3 +1898,220 @@ const WRITE_LAYER = [
1898
1898
  }, 150000);
1899
1899
  }
1900
1900
  });
1901
+ // ═══════════════════════════════════════════════════════════════
1902
+ // E1:类方法的**真实调用**必须进 IR(2026-09-21)
1903
+ //
1904
+ // 命名说明:C4k 已被占用(容器承载轴「属性写入 ⇒ 容器传播」,实测收益上限 0,
1905
+ // 判定不修,见设计稿 §十七)。本组是**提取器主干**,另开 E(Extractor)系列。
1906
+ //
1907
+ // 背景(Claude 补充的历史事实):主 IR 循环的类方法分支自该功能诞生之日起
1908
+ // 只调 computeMarkerCalls(产 __progmune_* 语义标记),从未调 extractDirectCalls
1909
+ // (产真实调用名);函数声明 / 箭头 / 包装箭头三个分支都是两行齐全。
1910
+ // 结果:类方法的 calls 里永远只有标记、没有真实调用 ⇒ **状态机在 OO 代码上
1911
+ // 拿不到输入**。FP 观测池实测:NestJS 切片 61 个函数里 59 个 calls 为空(97%),
1912
+ // 池合计 197 个里 140 个空。此前未暴露是因为 taintpath 十一族语料全是函数载体。
1913
+ //
1914
+ // 判据:类方法条目的 calls 必须同时含「真实调用名」与「语义标记」——
1915
+ // 本轮是**补**真实调用,不是拿它替换标记(负对照·②守这条)。
1916
+ // ═══════════════════════════════════════════════════════════════
1917
+ (0, vitest_1.describe)("E1:类方法的真实调用必须进 IR(此前只有标记,状态机拿不到输入)", () => {
1918
+ const cases = [
1919
+ // ── 正例:此前全空的五种已知形态 ──────────────────────────
1920
+ {
1921
+ name: "正例·① 类方法(同步)",
1922
+ fn: "C.m",
1923
+ files: {
1924
+ "a.ts": `
1925
+ function helper() { return 1; }
1926
+ export class C { m() { helper(); } }
1927
+ `,
1928
+ },
1929
+ want: ["helper"],
1930
+ why: "最基础的类方法;此前 calls 为空",
1931
+ },
1932
+ {
1933
+ name: "正例·② 类方法(async / await)",
1934
+ fn: "C.m",
1935
+ files: {
1936
+ "a.ts": `
1937
+ function helper() { return 1; }
1938
+ export class C { async m() { await helper(); } }
1939
+ `,
1940
+ },
1941
+ want: ["helper"],
1942
+ why: "async 方法体同样是 Block,此前一样空",
1943
+ },
1944
+ {
1945
+ name: "正例·③ 类方法经 this 成员调用",
1946
+ fn: "C.m",
1947
+ files: {
1948
+ "a.ts": `
1949
+ export class C { private repo: any; m() { this.repo.save(); } }
1950
+ `,
1951
+ },
1952
+ want: ["save"],
1953
+ why: "NestJS/TypeORM 的实际写法;属性访问取末端名",
1954
+ },
1955
+ {
1956
+ name: "正例·④ 类方法带装饰器",
1957
+ fn: "C.m",
1958
+ files: {
1959
+ "a.ts": `
1960
+ function helper() { return 1; }
1961
+ function Get(): any { return () => {}; }
1962
+ export class C { @Get() m() { helper(); } }
1963
+ `,
1964
+ },
1965
+ want: ["helper"],
1966
+ why: "装饰器不得影响方法体遍历",
1967
+ },
1968
+ {
1969
+ name: "正例·⑤ 构造函数注入(方法用注入进来的依赖)",
1970
+ fn: "C.m",
1971
+ files: {
1972
+ "a.ts": `
1973
+ export class C { constructor(private repo: any) {} m() { this.repo.save(); } }
1974
+ `,
1975
+ },
1976
+ want: ["save"],
1977
+ why: "DI 形态;探针里五种已知失效形态之一",
1978
+ },
1979
+ // ── 正例:顺手测到的、没人提过的形状 ──────────────────────
1980
+ {
1981
+ name: "正例·⑥ static 方法",
1982
+ fn: "C.m",
1983
+ files: {
1984
+ "a.ts": `
1985
+ function helper() { return 1; }
1986
+ export class C { static m() { helper(); } }
1987
+ `,
1988
+ },
1989
+ want: ["helper"],
1990
+ why: "static 也在 getMethods() 内,同属本轮",
1991
+ },
1992
+ {
1993
+ name: "正例·⑦ private 方法",
1994
+ fn: "C.m",
1995
+ files: {
1996
+ "a.ts": `
1997
+ function helper() { return 1; }
1998
+ export class C { private m() { helper(); } }
1999
+ `,
2000
+ },
2001
+ want: ["helper"],
2002
+ why: "可见性修饰符不得影响提取",
2003
+ },
2004
+ {
2005
+ name: "正例·⑧ 抽象类里的实现方法",
2006
+ fn: "C.m",
2007
+ files: {
2008
+ "a.ts": `
2009
+ function helper() { return 1; }
2010
+ export abstract class C { abstract a(): void; m() { helper(); } }
2011
+ `,
2012
+ },
2013
+ want: ["helper"],
2014
+ why: "abstract 无体,但有体的方法照旧要提",
2015
+ },
2016
+ {
2017
+ name: "正例·⑨ 链式调用 this.a.b()",
2018
+ fn: "C.m",
2019
+ files: {
2020
+ "a.ts": `
2021
+ export class C { a: any; m() { this.a.b(); } }
2022
+ `,
2023
+ },
2024
+ want: ["b"],
2025
+ why: "末端名即被调用的方法名",
2026
+ },
2027
+ {
2028
+ name: "正例·⑩ 计算属性名方法",
2029
+ fn: "C.[K]",
2030
+ files: {
2031
+ "a.ts": `
2032
+ function helper() { return 1; }
2033
+ const K = "m";
2034
+ export class C { [K]() { helper(); } }
2035
+ `,
2036
+ },
2037
+ want: ["helper"],
2038
+ why: "名字解不解得出是另一回事,calls 不该受名字影响",
2039
+ },
2040
+ // ── 正对照:既有载体不得被误伤 ────────────────────────────
2041
+ {
2042
+ name: "正对照·⑪ 函数声明(基线)",
2043
+ fn: "plain",
2044
+ files: {
2045
+ "a.ts": `
2046
+ function helper() { return 1; }
2047
+ export function plain() { helper(); }
2048
+ `,
2049
+ },
2050
+ want: ["helper"],
2051
+ why: "本轮只补类方法,函数声明分支不得回归",
2052
+ },
2053
+ {
2054
+ name: "正对照·⑫ 箭头函数(基线)",
2055
+ fn: "arrow",
2056
+ files: {
2057
+ "a.ts": `
2058
+ function helper() { return 1; }
2059
+ export const arrow = () => { helper(); };
2060
+ `,
2061
+ },
2062
+ want: ["helper"],
2063
+ why: "同上;箭头分支不得回归",
2064
+ },
2065
+ // ── 负对照:防修过头 ─────────────────────────────────────
2066
+ {
2067
+ name: "负对照·① 嵌套箭头回调里的调用仍不提取",
2068
+ fn: "C.m",
2069
+ files: {
2070
+ "a.ts": `
2071
+ function helper() { return 1; }
2072
+ export class C { m() { setTimeout(() => { helper(); }, 0); } }
2073
+ `,
2074
+ },
2075
+ want: ["setTimeout"],
2076
+ dontWant: ["helper"],
2077
+ why: "既有设计:extractDirectCalls 遇嵌套函数即 skip。已实测函数声明/箭头/类方法三者口径一致 ⇒ 本轮不动",
2078
+ },
2079
+ {
2080
+ name: "负对照·② 真实调用不得挤掉语义标记",
2081
+ fn: "Uploader.read",
2082
+ files: {
2083
+ "handler.ts": `
2084
+ import * as fs from "fs";
2085
+ export class Uploader {
2086
+ read(filePath: string) {
2087
+ return fs.readFileSync(filePath, "utf-8");
2088
+ }
2089
+ }
2090
+ export function dispatchTool(args: any) {
2091
+ const target = params.arguments.file_path;
2092
+ return new Uploader().read(target);
2093
+ }
2094
+ `,
2095
+ },
2096
+ want: ["readFileSync", PATH_MARK],
2097
+ why: "本轮是**补**真实调用,标记必须仍在——否则污点轴会整体倒退。" +
2098
+ "(写这条用例时先跑探针确认判据:marker 只在**调用方把污点传进来**时才注入到" +
2099
+ "被调用方,无调用方的类方法本就无 marker —— 探针 ①/④ 一致、②/③ 一致)",
2100
+ },
2101
+ ];
2102
+ for (const c of cases) {
2103
+ (0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
2104
+ const dir = makeProject(c.files);
2105
+ try {
2106
+ const calls = marksFor(dir, c.fn);
2107
+ for (const w of c.want)
2108
+ (0, vitest_1.expect)(calls).toContain(w);
2109
+ for (const d of c.dontWant ?? [])
2110
+ (0, vitest_1.expect)(calls).not.toContain(d);
2111
+ }
2112
+ finally {
2113
+ fs.rmSync(dir, { recursive: true, force: true });
2114
+ }
2115
+ }, 150000);
2116
+ }
2117
+ });
@@ -338,6 +338,18 @@ function manualResolveModule(moduleSpecifier, sourceFilePath, projectRoot, origi
338
338
  }
339
339
  return null;
340
340
  }
341
+ /**
342
+ * 提取函数体内**真实出现的调用名**(区别于 computeMarkerCalls 产出的语义标记)。
343
+ *
344
+ * 2026-09-21(类方法空转修复):形参类型补 MethodDeclaration。
345
+ * 此前本函数只对函数声明 / 箭头 / 包装箭头三个载体调用过;主 IR 循环里的
346
+ * **类方法分支从来没调过它**,只调了 computeMarkerCalls ⇒ 类方法的 calls 里
347
+ * 永远只有标记、没有真实调用 ⇒ 状态机在 OO 代码上拿不到输入(FP 观测池实测:
348
+ * NestJS 切片 61 个函数里 59 个 calls 为空)。该空转自类方法提取功能诞生即存在,
349
+ * 因 taintpath 十一族语料全是函数载体而从未现形。
350
+ * MethodDeclaration 与 FunctionDeclaration 一样具备 getBody() / getText(),
351
+ * 遍历逻辑无需改动。
352
+ */
341
353
  function extractDirectCalls(func, preText) {
342
354
  const body = func.getBody();
343
355
  if (!body)
@@ -2861,7 +2873,10 @@ function _extractSingleProject(absRoot, tsconfigPath) {
2861
2873
  continue;
2862
2874
  const mParams = m.getParameters();
2863
2875
  const mText = m.getText();
2864
- const mCalls = computeMarkerCalls(mText, mParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns, directGuardFns, shapers);
2876
+ // 2026-09-21:补上真实调用提取(此前只有 computeMarkerCalls,见 extractDirectCalls 注释)。
2877
+ // 顺序与函数声明 / 箭头两个分支保持一致:先真实调用,再追加语义标记。
2878
+ const mCalls = extractDirectCalls(m, mText);
2879
+ mCalls.push(...computeMarkerCalls(mText, mParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns, directGuardFns, shapers));
2865
2880
  funcs.push({
2866
2881
  name: `${cn}.${mn}`,
2867
2882
  params: mParams.map((p) => ({ name: p.getName(), type: getParamType(p), typeDetail: getParamTypeDetail(p) })),
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.46";
23
+ exports.RUNTIME_VERSION = "3.7.48";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
@@ -103,7 +103,7 @@ async function evaluateTrust(ctx) {
103
103
  // ── Phase 5: Build call graph for cross-function propagation ──
104
104
  const callGraph = (0, call_graph_propagator_1.buildCallGraphFromIR)(path.join(ctx.projectPath, "ir.json"));
105
105
  const enterpriseViolations = collectEnterpriseViolations(ctx);
106
- const { violations: protocolViolations, coverage: mappingCoverageData, ssgCoverage: ssgCov, annotationSuggestions: cAnnotationSuggestions } = await collectProtocolViolations(ctx, callGraph);
106
+ const { violations: protocolViolations, coverage: mappingCoverageData, ssgCoverage: ssgCov, annotationSuggestions: cAnnotationSuggestions, extractionWarning } = await collectProtocolViolations(ctx, callGraph);
107
107
  const expressResult = collectExpressViolations(ctx);
108
108
  const nestjsResult = collectNestJSViolations(ctx);
109
109
  const trpcResult = collectTRPCViolations(ctx);
@@ -218,12 +218,20 @@ async function evaluateTrust(ctx) {
218
218
  score: effectiveScore,
219
219
  decision,
220
220
  confidence,
221
- coverageConfidence: {
222
- score: coverageConfidence.score,
223
- margin: coverageConfidence.margin,
224
- level: coverageConfidence.level,
225
- summary: coverageConfidence.summary,
226
- },
221
+ coverageConfidence: extractionWarning
222
+ ? {
223
+ score: 0,
224
+ margin: 25,
225
+ level: "LOW",
226
+ summary: `${extractionWarning}——本次扫描是废票,不得据此得出「干净」结论`,
227
+ }
228
+ : {
229
+ score: coverageConfidence.score,
230
+ margin: coverageConfidence.margin,
231
+ level: coverageConfidence.level,
232
+ summary: coverageConfidence.summary,
233
+ },
234
+ extractionWarning,
227
235
  mappingCoverage: mappingCoverageData.totalApis > 0
228
236
  ? {
229
237
  rate: Math.round(((mappingCoverageData.lookupHits + mappingCoverageData.llmHits) /
@@ -1208,6 +1216,8 @@ async function collectProtocolViolations(ctx, callGraph) {
1208
1216
  const annotationRuleNames = new Set();
1209
1217
  // C 注解建议(函数级作用域——返回值在 try 外组装)
1210
1218
  let annotationSuggestions;
1219
+ // 2026-09-22:提取失败标记(跨 try 作用域,随结果返回)
1220
+ let extractionWarning;
1211
1221
  try {
1212
1222
  // ── P4.5 校准:TS/JS 项目在 ir.json 缺失时先提取 IR ──
1213
1223
  // 序列必须来自「函数体内真实调用」而非「文件内函数声明名单」——
@@ -1255,10 +1265,35 @@ async function collectProtocolViolations(ctx, callGraph) {
1255
1265
  `需重提取运行 PROGMUNE_IR_REEXTRACT=always 或删除 ir.json)`);
1256
1266
  }
1257
1267
  if (shouldExtract) {
1258
- const ir = extractFn();
1259
- // C 走合并形态(与 execute/MCP 写盘一致);TS/Python 保持裸数组形态
1260
- const payload = lang === "c" ? { typeMap: {}, functions: ir } : ir;
1261
- fs.writeFileSync(path.join(ctx.projectPath, "ir.json"), JSON.stringify(payload, null, 2));
1268
+ try {
1269
+ const ir = extractFn();
1270
+ // C 走合并形态(与 execute/MCP 写盘一致);TS/Python 保持裸数组形态
1271
+ const payload = lang === "c" ? { typeMap: {}, functions: ir } : ir;
1272
+ fs.writeFileSync(path.join(ctx.projectPath, "ir.json"), JSON.stringify(payload, null, 2));
1273
+ }
1274
+ catch (extractErr) {
1275
+ // 2026-09-22:提取异常不再静默吞掉——OOM 杀(exit 137)等失败
1276
+ // 必须发声,否则引擎带着空 IR 输出「0 违规」的假干净结论
1277
+ extractionWarning = `IR 提取失败(${extractErr?.message || extractErr})——结果不可信`;
1278
+ }
1279
+ }
1280
+ // 2026-09-22:提取后验尸——ir.json 存在但函数数为 0 且项目确有
1281
+ // 源码时,说明提取被静默截断(如 python3 被 OOM 杀后留下 2 字节
1282
+ // 空数组),同样记为失败
1283
+ if (!extractionWarning) {
1284
+ try {
1285
+ const irPath2 = path.join(ctx.projectPath, "ir.json");
1286
+ if (fs.existsSync(irPath2)) {
1287
+ const raw = fs.readFileSync(irPath2, "utf-8");
1288
+ const parsed = JSON.parse(raw);
1289
+ const fnCount = Array.isArray(parsed) ? parsed.length : (parsed.functions || []).length;
1290
+ if (fnCount === 0 && freshness.scannedFiles > 0) {
1291
+ extractionWarning = `IR 提取产物为空(0 函数,但项目有 ${freshness.scannedFiles} 个源文件)——提取可能被 OOM 截断,结果不可信`;
1292
+ console.warn(`[progmune] ⚠️ ${extractionWarning}`);
1293
+ }
1294
+ }
1295
+ }
1296
+ catch { /* best-effort */ }
1262
1297
  }
1263
1298
  }
1264
1299
  catch { /* best-effort — 回退正则扫描 */ }
@@ -1590,6 +1625,7 @@ async function collectProtocolViolations(ctx, callGraph) {
1590
1625
  },
1591
1626
  } : undefined,
1592
1627
  annotationSuggestions,
1628
+ extractionWarning,
1593
1629
  };
1594
1630
  }
1595
1631
  function extractCallSequencesFromProject(projectPath, language, keepNames) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.46",
3
+ "version": "3.7.48",
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/",