progmune-runtime 3.6.1 → 3.7.1

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,39 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.1] — 2026-08-23
4
+
5
+ ### 修复:词段匹配门控(仅项目函数适用)
6
+
7
+ - `ssg-bridge` 的词段匹配(Strategy 2)增加 `projectFunctions` 门控:只对项目函数做词段匹配——它是为改名协议原语设计的(协议原语必然是项目内函数,如 S5 的 `create_active_session`),外部库调用(如 Node 的 `readFileSync`)经词段撞上 `read_file` 是纯噪声
8
+ - 外部 API 的语义桥接不受影响:alias 配置(Strategy 0b)与 domain 关键词(Strategy 3)照常工作;未提供集合时保持旧行为(向后兼容)
9
+ - 共享集合构造 `collectProjectFunctionNames`(`src/call-sequence.ts`,全名/裸名/小写变体三形态收录),生产引擎与协议盲测扫描器同款传入
10
+
11
+ ### 修复:合并形态 ir.json 恢复 IR-first(3.5.0 起静默回退的回归)
12
+
13
+ - `extractCallSequencesFromIR` 与项目 IR 注解合并块兼容 `{ typeMap, functions }` 合并对象(execute/MCP 写盘形态)——此前 `Array.isArray` 守卫使所有 TS 项目自 3.5.0 起静默走正则回退,P4.5/P4.6 的 IR-first 语义在合并形态下未生效
14
+ - 配合词段门控后实测:自身 1966 函数 451 入口序列,SSG 违规 346→**2**(均真实命中,`writeTrajectoryFile`→`write_file`),Trust 总分 60→83(APPROVED)
15
+ - 协议盲测 v1.2 复测零漂移:66 可测金标 64 检出(Recall 97% / Precision 100% / 0 FP),S5 改名检测不受门控影响
16
+
17
+ ## [3.7.0] — 2026-08-23
18
+
19
+ ### 新增:P4.6 跨函数传播(入口展开 + 片段抑制)
20
+
21
+ - `src/call-sequence.ts`:`buildCallSequences` 共享序列构建——入口函数(不被项目函数调用)的调用链做传递展开(内联被调项目函数体,深度 ≤4、环安全);非入口函数的孤立片段不再单独验证(违规归因到调用它的入口),消除 helper 片段误报
22
+ - 规则名函数与叶子原语(函数体只调外部调用)不内联——协议原语只在调用链内验证,调用名保留给匹配层
23
+ - trust 引擎接线:`extractCallSequencesFromIR` 换用 `buildCallSequences`,规则名集合作为展开保留单元;生效范围如实记录——ir.json 为函数数组形态(协议盲测语料 / extractIR 直出)时 P4.6 生效;合并形态 `{ typeMap, functions }`(execute/MCP 写盘)沿用既有回退路径(3.5.0 起的既有行为,恢复 IR-first 需先做词段匹配门控的 FP 打磨)
24
+ - 边界(与 C 的 L3 同类,如实记录):展开是语法内联(调用链扁平化),不做数据流/指针/分支分析
25
+
26
+ ### 新增:协议盲测 v1.2(跨函数 + 任意命名变体)
27
+
28
+ - 语料网格扩至 38 项目:T0–T5 × S1–S5(30)+ T6/T7 × S1–S4(8);新增违规类 T6 cross_function_precondition、T7 cross_function_cleanup、风格 S5 renamed(无 `@progmune` 注解 + 改名协议函数,词段匹配验证)
29
+ - **复测结果:66 可测金标,检出 64(Recall 97%)/ Precision 100% / 0 FP**;2 处漏检为 T2×S5 注解依赖前置约束(无注解项目级前置不可恢复,命名匹配本身正常),金标与基线如实单列
30
+ - 回归测试 `tests/python-protocol-benchmark.test.ts` 扩至 6 例(T1 broken / T0 clean 含分离式清洁链 / T5 endState / T6 cross-function / S5 renamed)
31
+
32
+ ### 文档
33
+
34
+ - 覆盖矩阵(中英)Python 协议行(Auth / Resource Lifecycle)由 ⚠️ 升级 ✅,证据引用协议盲测 v1.2;升级条件(跨函数传播、任意命名验证)全部勾选
35
+ - 基线 `BASELINE_PROTOCOL_PYTHON_v1.md` 更新至 v1.2:语料、结果、已知缺口(注解依赖 / LLM 桥接不在测量范围 / P4.6 展开语义边界)如实记录
36
+
3
37
  ## [3.6.1] — 2026-08-23
4
38
 
5
39
  ### 文档
@@ -13,17 +13,47 @@
13
13
  * 不做数据流/指针/分支分析;跨文件依赖 IR 的 calls[] 图。
14
14
  */
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.isProjectFn = isProjectFn;
17
+ exports.collectProjectFunctionNames = collectProjectFunctionNames;
16
18
  exports.buildCallSequences = buildCallSequences;
17
19
  const MAX_DEPTH = 4;
18
20
  /** 项目函数判定:有真实文件且非外部导入条目(external 条目无函数体可内联)。 */
19
21
  function isProjectFn(f) {
20
22
  return !f.external && !!f.file && f.file !== "(external)";
21
23
  }
24
+ /**
25
+ * 构建项目函数名集合——词段匹配门控用(ssg-bridge 的 projectFunctions 参数)。
26
+ * 每个项目函数收录三种形态:全名(FlowService.svc_x)、裸名(svc_x)与
27
+ * 小写变体(调用名大小写差异,如 createActiveSession vs createactivesession)。
28
+ */
29
+ function collectProjectFunctionNames(ir) {
30
+ const names = new Set();
31
+ for (const f of ir) {
32
+ if (!isProjectFn(f))
33
+ continue;
34
+ const name = String(f.name || "");
35
+ if (!name)
36
+ continue;
37
+ const lower = name.toLowerCase();
38
+ names.add(name);
39
+ names.add(lower);
40
+ const dotIdx = name.lastIndexOf(".");
41
+ if (dotIdx >= 0) {
42
+ const bare = name.slice(dotIdx + 1);
43
+ names.add(bare);
44
+ names.add(bare.toLowerCase());
45
+ }
46
+ }
47
+ return names;
48
+ }
22
49
  /**
23
50
  * 从 IR 构建验证序列:入口函数展开 + 非入口抑制。
24
51
  * @param ir - FunctionInfo 列表(TS 或 Python 提取器输出)
52
+ * @param keepNames - 协议规则名集合:命中这些名字的项目函数是验证单元
53
+ * (其调用名保留给规则匹配),不内联其函数体——否则 create_session 等
54
+ * 规则函数的平凡函数体会把调用名"吞掉"
25
55
  */
26
- function buildCallSequences(ir) {
56
+ function buildCallSequences(ir, keepNames) {
27
57
  const fnMap = new Map();
28
58
  for (const f of ir) {
29
59
  if (isProjectFn(f))
@@ -39,19 +69,30 @@ function buildCallSequences(ir) {
39
69
  calledBy.add(c);
40
70
  }
41
71
  }
42
- const expand = (name, depth, visiting) => {
43
- if (depth > MAX_DEPTH || visiting.has(name))
44
- return [];
45
- const fn = fnMap.get(name);
46
- if (!fn)
47
- return [name]; // 外部调用:保留给规则匹配
48
- visiting.add(name);
72
+ /** 展开函数体内的调用(入口序列 = 函数体调用,不含函数自己的名字) */
73
+ const expandBody = (fn, depth, visiting) => {
49
74
  const out = [];
50
75
  for (const c of fn.calls || []) {
51
76
  if (typeof c !== "string" || c.startsWith("__progmune_"))
52
77
  continue;
53
- out.push(...expand(c, depth + 1, visiting));
78
+ out.push(...expandCall(c, depth, visiting));
54
79
  }
80
+ return out;
81
+ };
82
+ const expandCall = (name, depth, visiting) => {
83
+ if (depth > MAX_DEPTH || visiting.has(name))
84
+ return [];
85
+ const fn = fnMap.get(name);
86
+ // 外部调用或规则函数:调用名保留给匹配层,不内联
87
+ if (!fn || (keepNames && keepNames.has(name)))
88
+ return [name];
89
+ // 叶子函数(函数体只调外部原语)是协议原语或叶子 helper:
90
+ // 保留名字,不内联——否则 S5 改名协议函数的平凡函数体会吞掉调用名
91
+ const hasProjectCalls = (fn.calls || []).some((c) => fnMap.has(c));
92
+ if (!hasProjectCalls)
93
+ return [name];
94
+ visiting.add(name);
95
+ const out = expandBody(fn, depth + 1, visiting);
55
96
  visiting.delete(name);
56
97
  return out;
57
98
  };
@@ -61,7 +102,9 @@ function buildCallSequences(ir) {
61
102
  continue;
62
103
  if (calledBy.has(f.name))
63
104
  continue; // 非入口:片段并入调用方
64
- const calls = expand(f.name, 0, new Set());
105
+ if (keepNames && keepNames.has(f.name))
106
+ continue; // 协议原语不是入口:只在调用链内验证
107
+ const calls = expandBody(f, 0, new Set());
65
108
  if (calls.length === 0)
66
109
  continue;
67
110
  sequences.push({ calls, file: f.file, function: f.name });
@@ -62,6 +62,7 @@ const api_semantic_mapper_1 = require("./api-semantic-mapper");
62
62
  const protocol_domain_validator_1 = require("./protocol-domain-validator");
63
63
  const call_graph_propagator_1 = require("./call-graph-propagator");
64
64
  const ssg_bridge_1 = require("./ssg-bridge");
65
+ const call_sequence_1 = require("../call-sequence");
65
66
  // ── Main Entry Point ──
66
67
  async function evaluateTrust(ctx) {
67
68
  const engineVersion = "trust-runtime-v1.0.0";
@@ -719,12 +720,26 @@ async function collectProtocolViolations(ctx, callGraph) {
719
720
  }
720
721
  catch { /* best-effort — 回退正则扫描 */ }
721
722
  }
723
+ // ── SSG State Machine: load protocol rules once ──
724
+ protocolRulesData = (0, ssg_bridge_1.loadProtocolRules)(ctx.projectPath);
722
725
  // ── Phase 1-5 Semantic Pipeline ──
723
- const callSequences = extractCallSequencesFromProject(ctx.projectPath, ctx.language);
726
+ // 规则名集合作为展开的保留单元:规则函数不内联(调用名保留给匹配层)
727
+ const callSequences = extractCallSequencesFromProject(ctx.projectPath, ctx.language, protocolRulesData ? new Set(protocolRulesData.rules.keys()) : undefined);
724
728
  const flaggedCount = { value: 0 };
725
729
  const cleanCount = { value: 0 };
726
- // ── SSG State Machine: load protocol rules once ──
727
- protocolRulesData = (0, ssg_bridge_1.loadProtocolRules)(ctx.projectPath);
730
+ // ── P4.6.1: 词段匹配门控的项目函数集合(best-effort,与注解合并共用 ir.json) ──
731
+ // 词段匹配只对项目函数适用(改名协议原语);外部库调用走 alias/关键词桥接。
732
+ let projectFunctions;
733
+ try {
734
+ const fs = require("fs");
735
+ const irPath = path.join(ctx.projectPath, "ir.json");
736
+ if (fs.existsSync(irPath)) {
737
+ const ir = JSON.parse(fs.readFileSync(irPath, "utf-8"));
738
+ const functions = Array.isArray(ir) ? ir : (ir.functions || []);
739
+ projectFunctions = (0, call_sequence_1.collectProjectFunctionNames)(functions);
740
+ }
741
+ }
742
+ catch { /* best-effort */ }
728
743
  // ── P4.5: 合并项目 IR 注解协议(IR 优先,缺 namespace 继承内置 JSON) ──
729
744
  // 内置 protocols.json 的规则是通用弱约束(如 generate_jwt pre=[]),
730
745
  // 项目文件里的 @protocol 注解才是项目真实协议(如 pre=[PASSWORD_VERIFIED])。
@@ -735,17 +750,18 @@ async function collectProtocolViolations(ctx, callGraph) {
735
750
  const irPath = path.join(ctx.projectPath, "ir.json");
736
751
  if (fs.existsSync(irPath)) {
737
752
  const ir = JSON.parse(fs.readFileSync(irPath, "utf-8"));
738
- if (Array.isArray(ir)) {
739
- for (const f of ir) {
740
- if (!f.protocol)
741
- continue;
742
- const protocol = { ...f.protocol };
743
- const existing = protocolRulesData.rules.get(String(f.name));
744
- if (existing?.namespace && !protocol.namespace) {
745
- protocol.namespace = existing.namespace;
746
- }
747
- protocolRulesData.rules.set(String(f.name), protocol);
753
+ // ir.json 两种形态:extractIR/extractIRPython 的裸数组、extractProjectIR
754
+ // { typeMap, functions } 合并对象(execute/MCP 写盘)——统一取函数列表。
755
+ const functions = Array.isArray(ir) ? ir : (ir.functions || []);
756
+ for (const f of functions) {
757
+ if (!f.protocol)
758
+ continue;
759
+ const protocol = { ...f.protocol };
760
+ const existing = protocolRulesData.rules.get(String(f.name));
761
+ if (existing?.namespace && !protocol.namespace) {
762
+ protocol.namespace = existing.namespace;
748
763
  }
764
+ protocolRulesData.rules.set(String(f.name), protocol);
749
765
  }
750
766
  }
751
767
  }
@@ -842,7 +858,7 @@ async function collectProtocolViolations(ctx, callGraph) {
842
858
  // to bridge real API names to abstract protocol function names.
843
859
  if (protocolRulesData) {
844
860
  try {
845
- const ssgResult = (0, ssg_bridge_1.validateSequenceWithSSG)(semantic.steps, protocolRulesData.rules, protocolRulesData.namespaceInitialStates, seq.file, protocolRulesData.aliasIndex, protocolRulesData.wildcardAliases);
861
+ const ssgResult = (0, ssg_bridge_1.validateSequenceWithSSG)(semantic.steps, protocolRulesData.rules, protocolRulesData.namespaceInitialStates, seq.file, protocolRulesData.aliasIndex, protocolRulesData.wildcardAliases, projectFunctions);
846
862
  ssgResults.push(ssgResult);
847
863
  ssgTotalCalls += ssgResult.stats.totalCalls;
848
864
  ssgMatchedCalls += ssgResult.stats.matchedCalls;
@@ -904,40 +920,33 @@ async function collectProtocolViolations(ctx, callGraph) {
904
920
  } : undefined,
905
921
  };
906
922
  }
907
- function extractCallSequencesFromProject(projectPath, language) {
908
- // P4.5: 优先 IR 精确序列(每个函数体内的真实调用,从各自入口验证)
909
- const irSequences = extractCallSequencesFromIR(projectPath);
923
+ function extractCallSequencesFromProject(projectPath, language, keepNames) {
924
+ // P4.5/P4.6: 优先 IR 精确序列(入口函数展开 + 非入口抑制,跨函数传播)
925
+ const irSequences = extractCallSequencesFromIR(projectPath, keepNames);
910
926
  if (irSequences.length > 0)
911
927
  return irSequences;
912
928
  // 回退:正则扫描(C 等无 IR 语言保持原行为)
913
929
  return extractCallSequencesRegex(projectPath, language);
914
930
  }
915
931
  /**
916
- * P4.5: 从 ir.json 构建 per-function 调用序列。
917
- * 每个函数体内的 calls[] 是一条独立序列(从协议初始状态起步验证)——
918
- * 函数声明名单不再是验证对象;语义 marker(__progmune_*)供规则消费,不作真实调用。
932
+ * P4.5/P4.6: 从 ir.json 构建验证序列——入口函数展开 + 非入口抑制
933
+ * (共享实现见 src/call-sequence.ts buildCallSequences):
934
+ * 函数声明名单不再是验证对象;语义 marker(__progmune_*)供规则消费,
935
+ * 不作真实调用;被项目函数调用的函数片段并入调用方展开序列。
919
936
  */
920
- function extractCallSequencesFromIR(projectPath) {
937
+ function extractCallSequencesFromIR(projectPath, keepNames) {
921
938
  try {
922
939
  const fs = require("fs");
923
940
  const irPath = path.join(projectPath, "ir.json");
924
941
  if (!fs.existsSync(irPath))
925
942
  return [];
926
943
  const ir = JSON.parse(fs.readFileSync(irPath, "utf-8"));
927
- if (!Array.isArray(ir))
944
+ // ir.json 两种形态兼容:裸数组(extractIR / extractIRPython)与
945
+ // { typeMap, functions } 合并对象(extractProjectIR,execute/MCP 写盘)。
946
+ const functions = Array.isArray(ir) ? ir : (ir.functions || []);
947
+ if (!Array.isArray(functions) || functions.length === 0)
928
948
  return [];
929
- const sequences = [];
930
- for (const f of ir) {
931
- const calls = (f.calls || []).filter((c) => typeof c === "string" && !c.startsWith("__progmune_"));
932
- if (calls.length === 0)
933
- continue;
934
- sequences.push({
935
- calls,
936
- file: String(f.file || ""),
937
- function: String(f.name || "unknown"),
938
- });
939
- }
940
- return sequences;
949
+ return (0, call_sequence_1.buildCallSequences)(functions, keepNames);
941
950
  }
942
951
  catch {
943
952
  return [];
@@ -227,7 +227,7 @@ function normalizeName(name) {
227
227
  * 2. Substring match (call contains rule name or vice versa)
228
228
  * 3. Domain-guided keyword match
229
229
  */
230
- function inferRuleName(apiName, domain, description, ruleNames, namespace, aliases, wildcardAliases) {
230
+ function inferRuleName(apiName, domain, description, ruleNames, namespace, aliases, wildcardAliases, projectFunctions) {
231
231
  const normalized = normalizeName(apiName);
232
232
  const lowerApi = apiName.toLowerCase();
233
233
  const lowerDesc = description.toLowerCase();
@@ -262,7 +262,20 @@ function inferRuleName(apiName, domain, description, ruleNames, namespace, alias
262
262
  // word segment in the normalized call name. This prevents "status" from
263
263
  // matching "poll_status" via raw substring, since "status" must appear
264
264
  // as a complete _-delimited segment.
265
+ //
266
+ // 门控(P4.6.1):词段匹配只对「项目函数」适用——它是为改名协议原语设计的
267
+ // (协议原语必然是项目内函数,如 S5 的 create_active_session)。外部库调用
268
+ // (如 Node 的 readFileSync)经词段撞上 read_file 是纯噪声:外部 API 的语义
269
+ // 桥接走 alias 配置(Strategy 0b)或 domain 关键词(Strategy 3),不走词段。
270
+ // 未提供 projectFunctions 时保持旧行为(向后兼容测试与无 IR 的调用方)。
271
+ const isProjectFn = !projectFunctions
272
+ || projectFunctions.has(apiName)
273
+ || projectFunctions.has(lowerApi)
274
+ || projectFunctions.has(normalized)
275
+ || (dotIdx >= 0 && projectFunctions.has(lowerApi.slice(dotIdx + 1)));
265
276
  for (const ruleName of ruleNames) {
277
+ if (projectFunctions && !isProjectFn)
278
+ continue;
266
279
  const ruleWords = ruleName.split("_");
267
280
  const callWords = normalized.split("_");
268
281
  // Rule must have at least 2 words for this strategy (single-word rules
@@ -303,7 +316,9 @@ function inferRuleName(apiName, domain, description, ruleNames, namespace, alias
303
316
  * @param file — source file path (for violation reporting)
304
317
  * @returns SSG validation result with violations and trace
305
318
  */
306
- function validateSequenceWithSSG(steps, rules, namespaceInitialStates, file, aliasIndex, wildcardAliases) {
319
+ function validateSequenceWithSSG(steps, rules, namespaceInitialStates, file, aliasIndex, wildcardAliases,
320
+ /** 项目函数名集合(含裸名/全名/小写变体由调用方构造)——提供后词段匹配只对项目函数适用 */
321
+ projectFunctions) {
307
322
  const allRuleNames = Array.from(rules.keys());
308
323
  const aliases = aliasIndex || new Map();
309
324
  const wildcards = wildcardAliases || new Map();
@@ -361,7 +376,7 @@ function validateSequenceWithSSG(steps, rules, namespaceInitialStates, file, ali
361
376
  // Even if the semantic domain maps to "stateless", an alias may still
362
377
  // map this call to a specific protocol rule with its own namespace.
363
378
  // Priority: wildcard alias → exact alias → name match → word match → keyword
364
- const ruleName = inferRuleName(step.api, step.domain, step.description, allRuleNames, namespace, aliases, wildcards);
379
+ const ruleName = inferRuleName(step.api, step.domain, step.description, allRuleNames, namespace, aliases, wildcards, projectFunctions);
365
380
  // Use matched rule's namespace, falling back to domain-inferred namespace
366
381
  const effectiveNamespace = ruleName
367
382
  ? (rules.get(ruleName)?.namespace || namespace)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.6.1",
3
+ "version": "3.7.1",
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/",