progmune-runtime 3.7.42 → 3.7.43

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,69 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.43] — 2026-09-20
4
+
5
+ ### C4g:helper 换个载体(对象字面量方法 / 类方法 / 解构形参)
6
+
7
+ helper 不一定是函数,也可能是方法:
8
+
9
+ ```ts
10
+ export const Util = { toPath(n: string): string { return n + ".md"; } }; // 对象字面量方法
11
+ export class Renderer { inst(n: string): string { return n + ".md"; } } // 类实例方法
12
+ ```
13
+
14
+ C4e 时刻意没做这一路(按裸名放行会让别的对象上的同名方法被误认成塑形,R11)。
15
+ 这一轮用**限定名 + 唯一性**解决:
16
+
17
+ - 宿主名能确定时登记 `Owner.method`,调用点 `Util.toPath(k)` 直接对上;
18
+ - 实例方法的主要写法是 `r.inst(k)`(宿主是变量,限定名对不上),所以该方法名在
19
+ **全项目唯一**时额外允许 `.inst(` 这种成员调用位匹配 —— 唯一性是 R11 的替代品,
20
+ 不存在同名方法就不可能张冠李戴。同名方法出现两次时一律不登记,宁可漏报。
21
+
22
+ 另补**解构形参**:`function f({ name }) {…}` 的 `getName()` 给的是整个绑定模式
23
+ 而不是 `name`,要从 BindingElement 里逐个抽出来,否则这类 helper 一个都认不出。
24
+
25
+ R12 第四次生效:备忘里列的「依赖分析深层形态」(三元返回、默认参、多 return)
26
+ 探针实测**本来就是通的**,真漏的只有解构形参。
27
+
28
+ ### 语料踩到的坑:helper 名字不能撞 sink 名单
29
+
30
+ H 族第一版把静态方法写成 `Renderer.stat(k)`,闸门全绿;反向验证摘掉方法收集之后
31
+ 它**仍然标**。一查:`stat` 就在 `TS_FILE_SINK_NAMES` 里,调用点被 sink 兜底直接
32
+ 命中,与 helper 传播毫无关系 —— 实测 `Other.stat(k)`(接收者根本没定义)、以及类里
33
+ 压根没有 `stat` 方法,照样标。
34
+
35
+ 这是 R13 那条旁路的第二次出现,而且更阴:它让正例「标对了」,但标对的原因是另一
36
+ 回事。改名为 `toFile` 之后归因才干净。已登记为 **R16**。
37
+
38
+ ### 验收
39
+
40
+ | 门 | 结果 |
41
+ |---|---|
42
+ | taintpath 闸门 | **92/92**(新增 H 族 11 条:7 mark / 3 no-taint / 1 suppressed) |
43
+ | fr-007 真实语料 | pre 5 / post 0 **维持** |
44
+ | fr-016 真实语料 | pre 7 / post 0 **维持** —— 放宽召回后守卫侧仍无新增误报 |
45
+ | 定向用例 | **171 passed / 7 个测试文件**(新增 C4g 组 7 条:4 正 3 负) |
46
+ | TS 盲测 | 108 项目,path_traversal **LOST 0 / ADDED 27**(H 7 + E 9 + F 6 + G 4 + D 1),基线那 104 个项目**零漂移零丢失** |
47
+ | tsc | 零错误 |
48
+
49
+ ### 反向验证四刀
50
+
51
+ | 刀 | 改动 | 转红 |
52
+ |---|---|---|
53
+ | 1 | 摘掉方法候选收集 | 3 条方法正例 |
54
+ | 2 | 只摘掉成员调用位 `.method(` | emitClassInstance(宿主是变量那条) |
55
+ | 3 | 解构形参名提取退回整体文本 | emitDestructured |
56
+ | 4 | 关掉 C4f 收窄 | G 族 6 条 + H 族 `emitObjMethodDrop` |
57
+
58
+ 刀 4 顺带证明 C4f 的收窄对新载体同样生效 —— `Drop.fixed(k)` 不是空转对照。
59
+
60
+ ### 刻意不做:常量实参代入
61
+
62
+ `viaTernaryDrop(n, flag) { return flag ? "fixed.md" : n + ".md"; }` 在
63
+ `viaTernaryDrop(k, true)` 下走的是常量分支、形参不流出,现在仍会被判污染。要收这条
64
+ 得在调用点做常量传播(把实参字面量代入 return 再重算依赖),是另一个量级的改动,
65
+ 且代入出错的代价是漏报。登记为已知缺口。
66
+
3
67
  ## [3.7.42] — 2026-09-20
4
68
 
5
69
  ### C4f:名字级传播的精度收窄(形参-实参位置对齐 + 返回值依赖分析)
@@ -1380,3 +1380,60 @@ const WRITE_LAYER = [
1380
1380
  }, 150000);
1381
1381
  }
1382
1382
  });
1383
+ (0, vitest_1.describe)("C4g:helper 换个载体(对象字面量方法 / 类方法 / 解构形参)也要能传播", () => {
1384
+ const emit = (relExpr, pre = "") => [
1385
+ 'import * as fs from "fs";',
1386
+ 'import { Util, Drop, Renderer, viaName } from "./helpers";',
1387
+ "export function emit(doc: Record<string, any>, outDir: string) {",
1388
+ ...(pre ? [` ${pre}`] : []),
1389
+ " Object.keys(doc).forEach((k) => {",
1390
+ ` const rel = ${relExpr};`,
1391
+ ' fs.writeFileSync(outDir + "/" + rel, "x");',
1392
+ " });",
1393
+ "}",
1394
+ ].join("\n");
1395
+ const HELPERS = [
1396
+ "export const Util = {",
1397
+ ' toPath(n: string): string { return n + ".md"; },',
1398
+ "};",
1399
+ "export const Drop = {",
1400
+ ' fixed(n: string): string { return "fixed.md"; },',
1401
+ "};",
1402
+ "export class Renderer {",
1403
+ ' inst(n: string): string { return n + ".md"; }',
1404
+ ' static stat(n: string): string { return n + ".txt"; }',
1405
+ "}",
1406
+ 'export function viaName({ name }: any): string { return name + ".md"; }',
1407
+ ].join("\n");
1408
+ const cases = [
1409
+ { name: "正例·对象字面量方法", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit("Util.toPath(k)") },
1410
+ expectMark: true, why: "宿主名可确证 ⇒ 走限定名 Owner.method" },
1411
+ { name: "正例·类实例方法", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit("r.inst(k)", "const r = new Renderer();") },
1412
+ expectMark: true, why: "宿主是变量,限定名对不上 ⇒ 靠方法名唯一 + 成员调用位" },
1413
+ { name: "正例·静态类方法", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit("Renderer.stat(k)") },
1414
+ expectMark: true, why: "与对象字面量同形,走限定名" },
1415
+ { name: "正例·解构形参", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit("viaName({ name: k })") },
1416
+ expectMark: true, why: "形参名要从绑定模式里抽出来 —— getName() 给的是整个模式而不是 name" },
1417
+ { name: "负对照·对象字面量方法丢弃形参", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit("Drop.fixed(k)") },
1418
+ expectMark: false, why: "召回放宽到方法之后,C4f 的收窄也要跟得上新载体;正对照 = 正例·对象字面量方法" },
1419
+ { name: "负对照·解构形参喂字面量", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit('viaName({ name: "lit" })') },
1420
+ expectMark: false, why: "无污点流入;正对照 = 正例·解构形参" },
1421
+ { name: "负对照·方法是真塑形但实参是字面量", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit('Util.toPath("static")') },
1422
+ expectMark: false, why: "传播得靠实参带污点" },
1423
+ ];
1424
+ for (const c of cases) {
1425
+ (0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
1426
+ const dir = makeProject(c.files);
1427
+ try {
1428
+ const marks = marksFor(dir, c.fn);
1429
+ if (c.expectMark)
1430
+ (0, vitest_1.expect)(marks).toContain(PATH_MARK);
1431
+ else
1432
+ (0, vitest_1.expect)(marks).not.toContain(PATH_MARK);
1433
+ }
1434
+ finally {
1435
+ fs.rmSync(dir, { recursive: true, force: true });
1436
+ }
1437
+ }, 150000);
1438
+ }
1439
+ });
@@ -772,6 +772,60 @@ function pathModuleAliases(project) {
772
772
  }
773
773
  return out;
774
774
  }
775
+ /**
776
+ * 形参名列表:解构形参(`function f({ name }) {…}`)不能用 getName() 直接取,
777
+ * ts-morph 给的是整个绑定模式,要把里面的 BindingElement 名字逐个抽出来。
778
+ */
779
+ function paramNamesOf(p) {
780
+ const nn = p.getNameNode();
781
+ if (ts_morph_1.Node.isIdentifier(nn))
782
+ return [nn.getText()];
783
+ const out = [];
784
+ for (const be of nn.getDescendantsOfKind(ts.SyntaxKind.BindingElement)) {
785
+ const n = be.getNameNode();
786
+ if (ts_morph_1.Node.isIdentifier(n))
787
+ out.push(n.getText());
788
+ }
789
+ return out;
790
+ }
791
+ /**
792
+ * 方法声明的宿主限定名:`const Util = { toPath(){} }` ⇒ "Util"、
793
+ * `const Cfg = { inner: { toPath(){} } }` ⇒ "Cfg.inner"、`class Paths {…}` ⇒ "Paths"。
794
+ * 取不出来就返回 null —— 认不出宿主的宁可不登记(保守侧)。
795
+ */
796
+ function ownerQualifier(md) {
797
+ const parent = md.getParent();
798
+ if (!parent)
799
+ return null;
800
+ if (ts_morph_1.Node.isClassDeclaration(parent))
801
+ return parent.getName() ?? null;
802
+ if (!ts_morph_1.Node.isObjectLiteralExpression(parent))
803
+ return null;
804
+ const parts = [];
805
+ let cur = parent;
806
+ for (let guard = 0; guard < 5; guard++) {
807
+ const up = cur.getParent();
808
+ if (!up)
809
+ return null;
810
+ if (ts_morph_1.Node.isPropertyAssignment(up)) {
811
+ const key = up.getNameNode?.();
812
+ const keyName = key
813
+ ? key.getText().replace(/^["'`]|["'`]$/g, "")
814
+ : up.getChildAtIndex(0).getText().replace(/^["'`]|["'`]$/g, "");
815
+ parts.unshift(keyName);
816
+ cur = up.getParent();
817
+ if (!cur)
818
+ return null;
819
+ continue;
820
+ }
821
+ if (ts_morph_1.Node.isVariableDeclaration(up)) {
822
+ parts.unshift(up.getName());
823
+ return parts.join(".");
824
+ }
825
+ return null;
826
+ }
827
+ return null;
828
+ }
775
829
  /** 按顶层逗号切分实参串(跳过字符串与已配平的括号,后者由调用方保证不含嵌套) */
776
830
  function splitTopLevelArgs(argText) {
777
831
  const out = [];
@@ -820,12 +874,12 @@ function splitTopLevelArgs(argText) {
820
874
  * 抹掉的是**证据**,`shaped` 判定仍看原式 —— helper 是不是塑形,与它的返回值
821
875
  * 依不依赖实参是两件事。
822
876
  */
823
- function maskNonDependentArgs(text, info) {
877
+ function maskNonDependentArgs(text, info, memberNames = new Set()) {
824
878
  if (info.size === 0)
825
879
  return text;
826
880
  let cur = text;
827
881
  for (let round = 0; round < 3; round++) {
828
- const next = maskNonDependentArgsOnce(cur, info);
882
+ const next = maskNonDependentArgsOnce(cur, info, memberNames);
829
883
  if (next === null)
830
884
  break;
831
885
  cur = next;
@@ -833,12 +887,15 @@ function maskNonDependentArgs(text, info) {
833
887
  return cur;
834
888
  }
835
889
  /** 抹掉**一个**最内层调用;没有任何调用需要抹时返回 null(供调用方收敛) */
836
- function maskNonDependentArgsOnce(text, info) {
890
+ function maskNonDependentArgsOnce(text, info, memberNames) {
837
891
  for (const [name, si] of info) {
838
- const re = new RegExp(`(?:^|[^\\w$.])${escapeRe(name)}\\s*\\(`, "g");
892
+ const re = memberNames.has(name)
893
+ ? new RegExp(`\\.${escapeRe(name)}\\s*\\(`, "g")
894
+ : new RegExp(`(?:^|[${name.includes(".") ? "^\\w$" : "^\\w$."}])${escapeRe(name)}\\s*\\(`, "g");
839
895
  let m;
840
896
  while ((m = re.exec(text)) !== null) {
841
- const nameStart = m.index + (m[0].startsWith(name) ? 0 : 1);
897
+ // 成员形态的匹配从 `.` 起算,替换也连点一起换掉
898
+ const nameStart = memberNames.has(name) ? m.index : m.index + (m[0].startsWith(name) ? 0 : 1);
842
899
  const open = m.index + m[0].length - 1; // 开括号位置
843
900
  let depth = 0;
844
901
  let close = -1;
@@ -872,10 +929,10 @@ function maskNonDependentArgsOnce(text, info) {
872
929
  * return 表达式真正依赖哪些形参:把「已知 helper 调用里不被依赖的实参」抹掉后,
873
930
  * 剩下的形参名就是会流进返回值的那些。`wrap(n) { return discard(n); }` ⇒ 空集。
874
931
  */
875
- function returnDeps(exprs, params, info) {
932
+ function returnDeps(exprs, params, info, memberNames = new Set()) {
876
933
  const deps = new Set();
877
934
  for (const raw of exprs) {
878
- const masked = maskNonDependentArgs(raw, info);
935
+ const masked = maskNonDependentArgs(raw, info, memberNames);
879
936
  for (const p of params) {
880
937
  if (new RegExp(`(?:^|[^\\w$.])${escapeRe(p)}(?:[^\\w$]|$)`).test(masked))
881
938
  deps.add(p);
@@ -926,7 +983,7 @@ function pureShaperFunctionNames(project) {
926
983
  const body = fn.getBody();
927
984
  if (!body)
928
985
  continue; // 重载签名 / declare 只有签名
929
- consider(name, fn.getParameters().map((p) => p.getName()), fn.getText(), body);
986
+ consider(name, fn.getParameters().flatMap(paramNamesOf), fn.getText(), body);
930
987
  }
931
988
  // (b) 变量承载的箭头 / 函数表达式 —— 现代 TS 里 helper 的主力写法
932
989
  // `export const toPath = (n: string) => n + ".md";`
@@ -943,9 +1000,47 @@ function pureShaperFunctionNames(project) {
943
1000
  const body = fnLike.getBody();
944
1001
  if (!body)
945
1002
  continue;
946
- consider(vd.getName(), fnLike.getParameters().map((p) => p.getName()), init.getText(), body);
1003
+ consider(vd.getName(), fnLike.getParameters().flatMap(paramNamesOf), init.getText(), body);
947
1004
  }
948
1005
  }
1006
+ // (c) 对象字面量方法 / 类方法 —— C4g。
1007
+ // `const Util = { toPath(n) {…} }`、`class Paths { toPath(n) {…} }` 都不在
1008
+ // getFunctions() 里,C4e 时刻意没做:按裸名放行会让别的对象上的同名方法被
1009
+ // 误认成塑形(R11)。这里只登记**限定名** Owner.method;只有当该方法名在全
1010
+ // 项目唯一(不存在同名方法)时,才额外允许 `.method(` 这种不写宿主名的
1011
+ // 成员调用 —— 实例方法的主要写法正是这个。
1012
+ const methodCands = [];
1013
+ for (const sf of project.getSourceFiles()) {
1014
+ for (const md of sf.getDescendantsOfKind(ts.SyntaxKind.MethodDeclaration)) {
1015
+ const bare = md.getName();
1016
+ if (!bare)
1017
+ continue;
1018
+ const body = md.getBody();
1019
+ if (!body)
1020
+ continue; // 重载签名 / declare
1021
+ const owner = ownerQualifier(md);
1022
+ methodCands.push({ qualified: owner ? `${owner}.${bare}` : null, bare, md });
1023
+ }
1024
+ }
1025
+ const bareCount = new Map();
1026
+ for (const m of methodCands)
1027
+ bareCount.set(m.bare, (bareCount.get(m.bare) ?? 0) + 1);
1028
+ const memberNames = new Set();
1029
+ for (const m of methodCands) {
1030
+ const params = m.md.getParameters().flatMap(paramNamesOf);
1031
+ const text = m.md.getText();
1032
+ const body = m.md.getBody();
1033
+ if (!body)
1034
+ continue;
1035
+ // 实例方法的主要写法是 `p.toPath(k)` —— 宿主名是变量,限定名匹配不到,
1036
+ // 所以唯一时额外登记「按成员调用位匹配」的裸名。
1037
+ if (bareCount.get(m.bare) === 1) {
1038
+ consider(m.bare, params, text, body);
1039
+ memberNames.add(m.bare);
1040
+ }
1041
+ if (m.qualified)
1042
+ consider(m.qualified, params, text, body);
1043
+ }
949
1044
  // 形参之外允许出现的自由标识符 = 模块级字面量常量 ∪ 确证的 path 模块别名
950
1045
  const extraIdents = moduleLiteralConstNames(project);
951
1046
  for (const a of pathModuleAliases(project))
@@ -961,7 +1056,10 @@ function pureShaperFunctionNames(project) {
961
1056
  if (!ok)
962
1057
  continue;
963
1058
  names.add(c.name);
964
- info.set(c.name, { params: c.paramList, deps: returnDeps(c.rets, c.paramList, info) });
1059
+ info.set(c.name, {
1060
+ params: c.paramList,
1061
+ deps: returnDeps(c.rets, c.paramList, info, memberNames),
1062
+ });
965
1063
  added = true;
966
1064
  }
967
1065
  if (!added)
@@ -977,7 +1075,7 @@ function pureShaperFunctionNames(project) {
977
1075
  const c = byName.get(name);
978
1076
  if (!c)
979
1077
  continue;
980
- const d = returnDeps(c.rets, si.params, info);
1078
+ const d = returnDeps(c.rets, si.params, info, memberNames);
981
1079
  if (d.size !== si.deps.size || [...d].some((x) => !si.deps.has(x))) {
982
1080
  si.deps = d;
983
1081
  changed = true;
@@ -986,7 +1084,7 @@ function pureShaperFunctionNames(project) {
986
1084
  if (!changed)
987
1085
  break;
988
1086
  }
989
- return { names, info };
1087
+ return { names, memberNames, info };
990
1088
  }
991
1089
  const TS_FILE_SINK_NAMES = [
992
1090
  "readFile", "readFileSync", "writeFile", "writeFileSync",
@@ -1213,8 +1311,13 @@ function taintedViaShaper(text, tainted, shapers) {
1213
1311
  const localShaperRe = shapers && shapers.names.size > 0
1214
1312
  ? new RegExp(`(?:^|[^\\w$.])(?:${[...shapers.names].map(escapeRe).join("|")})\\s*\\(`)
1215
1313
  : null;
1314
+ // C4g:成员调用位(`.toPath(`)—— 宿主是变量、限定名匹配不到时用
1315
+ const memberShaperRe = shapers && shapers.memberNames.size > 0
1316
+ ? new RegExp(`\\.(?:${[...shapers.memberNames].map(escapeRe).join("|")})\\s*\\(`)
1317
+ : null;
1216
1318
  // C4f:helper 调用里【返回值不依赖的实参】不构成污点证据。
1217
1319
  const shaperInfo = shapers?.info;
1320
+ const memberNames = shapers?.memberNames;
1218
1321
  let grewAny = false;
1219
1322
  for (let depth = 0; depth < 3; depth++) {
1220
1323
  // 注意:这里**不能**以 `tainted.size === 0` 提前退出 —— 种子是根模式
@@ -1235,11 +1338,12 @@ function taintedViaShaper(text, tainted, shapers) {
1235
1338
  continue;
1236
1339
  const shaped = TS_PATH_SHAPER_RE.test(rhs) ||
1237
1340
  TS_CONCAT_RE.test(rhs) ||
1238
- (localShaperRe !== null && localShaperRe.test(rhs));
1341
+ (localShaperRe !== null && localShaperRe.test(rhs)) ||
1342
+ (memberShaperRe !== null && memberShaperRe.test(rhs));
1239
1343
  // 证据看的是【抹掉不被依赖的实参之后】的 rhs:`discard(k)` 里 k 不流出,
1240
1344
  // 那条支路就不该算证据。`shaped` 仍看原式 —— helper 是不是塑形,与它的
1241
1345
  // 返回值依不依赖实参是两件事。
1242
- const evidence = shaperInfo ? maskNonDependentArgs(rhs, shaperInfo) : rhs;
1346
+ const evidence = shaperInfo && memberNames ? maskNonDependentArgs(rhs, shaperInfo, memberNames) : rhs;
1243
1347
  if (taintRe.test(evidence) && shaped) {
1244
1348
  tainted.add(name);
1245
1349
  added = true;
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.42";
23
+ exports.RUNTIME_VERSION = "3.7.43";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.42",
3
+ "version": "3.7.43",
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/",