progmune-runtime 3.4.1 → 3.6.0

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,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.6.0] — 2026-08-23
4
+
5
+ ### 新增:SSG endState 检查(序列末尾资源未释放)
6
+
7
+ - trust 桥接路径(`src/trust/ssg-bridge.ts`)补齐 endState 检测:函数序列末尾仍有未释放资源状态 → 违规(`endState: true`、`fixPath=[releaseFn]`、追加式修复文案、独立 rule_id `SSG_*_END_STATE_VIOLATION`)
8
+ - 与 planner 语义对齐:共享判定 `findHeldResourceStates` + `RESOURCE_NAMESPACE_RE` 入 `ssg-validator.ts`(planner 重构换用,语义不变)
9
+ - 边界:仅资源生命周期命名空间(auth/session 合法地以活跃会话结束不检查);仅本序列新获取的状态(继承自初始态不算泄漏)
10
+ - **Python 协议盲测 v1 复测:40/40 全检出(Recall/Precision 100%,0 FP)**,基线 `BASELINE_PROTOCOL_PYTHON_v1.md`
11
+
12
+ ### 新增:Python 协议盲测基准(v1)
13
+
14
+ - `blind-benchmark/generate-projects-protocol-python.ts` + `scan-protocol-python.ts` + `expand-gold-protocol-python.ts`:6 违规类型 × 4 结构风格 = 24 项目,金标 `annotations-protocol-python-v1.json`
15
+ - 测量生产 SSG 桥接校验器(确定性、无 LLM);回归测试 `tests/python-protocol-benchmark.test.ts`
16
+
17
+ ### 文档
18
+
19
+ - README 新增「社区与反馈」章节:讨论群二维码(`assets/wechat-group.png`,当前为占位图待替换真实群码)+ GitHub Issues 通道(中英双语)
20
+ - README 删除双峰(Two-Hump)内容:科学基础章节的双峰类比段与 P0-P3 节的双峰报告链接
21
+ - 覆盖矩阵(中英)刷新至 2026-08-23 并新增英文版 `coverage-matrix-en.md`;架构图规则数修正 140→148(与 protocols.json 实测一致)
22
+
23
+ ## [3.5.0] — 2026-08-22
24
+
25
+ ### 新增:多语言合并 IR(注册表式提取)
26
+
27
+ - `src/extract-project-ir.ts`:`LanguageExtractor` 注册表(detect + extract),`extractProjectIR` 合并所有检测到语言的 FunctionInfo——混合项目中 TS 与 Python 函数共存于同一 IR
28
+ - agent loop 感知路径(`extractIRWithDelta`)、`execute()` 的 ir.json 写盘、MCP server 统一走合并入口:Python 项目的函数协议链进入 agent 编排范围(此前 agent 侧 IR 仅 TS)
29
+ - `extractIRPython` 默认写临时文件(可选 `outPath`),不再覆盖项目根 ir.json;单语言提取失败不中断其余语言,全部失败才抛错(保留 execute 硬失败语义)
30
+ - 新增语言(Go/Java/Rust):实现 detect + extract → `LANGUAGE_EXTRACTORS` 注册一条 → 调用方零改动
31
+
32
+ ### 修复:function-synonyms 本地超时(遗留)
33
+
34
+ - `runBootstrapValidation` 无参调用结果缓存(同进程复用,语料重度测试 5 次重计算降为 1 次)
35
+ - vitest 改用 forks 池 + 4GB 堆上限(本地语料丰富时 threads 池触 V8 自适应堆上限 OOM);本地 7/7 通过(~24s)
36
+
37
+ ### 验证
38
+
39
+ - 相关套件 26/26;`npm run check` 0 失败;合并冒烟:progmune-runtime 自身 4043 函数(TS 1949 + Python 2094)
40
+
3
41
  ## [3.4.1] — 2026-08-22
4
42
 
5
43
  ### 修复:`npm run check` 四项失败根因
package/README.md CHANGED
@@ -106,6 +106,8 @@ Progmune is honest about what it can and cannot verify.
106
106
  | **C** | ⚠️ Research-only | Gold benchmark F1=16.5%. L3 cross-function experiment terminated; L4 not planned. See [C Language Status](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/c-language-status.md). |
107
107
  | **Go, Java** | ❌ None | Planned |
108
108
 
109
+ **Multi-language IR (registry-based).** TypeScript (ts-morph) and Python (AST) extractors are registered entries in one registry (`src/extract-project-ir.ts`); `extractProjectIR` merges every detected language into a single function IR shared by the agent loop, `execute()`, and the MCP server — the agent composes function-protocol chains across both languages. Adding a language (Go, Java, …) = register one extractor entry; callers don't change.
110
+
109
111
  **Framework adapters: 2/13.** Express ✅ and tRPC ✅ have dedicated detectors; Next.js has version-aware governance; NestJS is partial. Django, FastAPI and 8 more remain — framework adaptation is the #1 product gap.
110
112
 
111
113
  ### What Progmune does NOT cover (honest boundaries)
@@ -116,7 +118,7 @@ Progmune is honest about what it can and cannot verify.
116
118
  - **Framework internals** — well-known framework dispatch/cache machinery (e.g. django-unicorn internals) can produce a small number of boundary false positives; they are documented per-corpus in the benchmark gold files.
117
119
  - **Known failure boundaries are documented** rather than hidden: if Progmune cannot verify a language (e.g. Go), Confidence is lowered instead of pretending 100%.
118
120
 
119
- → [Full Coverage Matrix](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/coverage-matrix.md)
121
+ → [Full Coverage Matrix](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/coverage-matrix-en.md)
120
122
 
121
123
  ---
122
124
 
@@ -160,7 +162,7 @@ C analysis is **research-only**: gold benchmark F1=16.5% across 4 repos (curl, l
160
162
  - Bootstrapping deadlock broken: all 21 protocol namespaces now have rule vocabulary
161
163
  - `excludePatterns` + `languages` architecture for FP management
162
164
 
163
- → [Two-Hump Report](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/two-hump-report.md) · [P0-P3 Final Report](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/p0-p3-final-report.md)
165
+ → [P0-P3 Final Report](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/p0-p3-final-report.md)
164
166
 
165
167
  ---
166
168
 
@@ -172,11 +174,12 @@ SDK (src/sdk.ts) verify() → APPROVED / NEEDS_REVIEW / BLOCKED
172
174
  ├─ Policy Engine Enterprise policy enforcement (ALLOW/WARN/BLOCK)
173
175
  ├─ SSG Validator Protocol state machine verification
174
176
  ├─ Protocol Detector Regex-based protocol step detection (22 detectors)
175
- ├─ IR Extractors TypeScript (ts-morph) + Python (ast module) → function IR;
176
- source-level markers: taint tracking, import resolution,
177
- qualified call chains, cross-file template analysis
177
+ ├─ IR Extraction Registry-based: TS (ts-morph) + Python (ast module)
178
+ merged into one function IR per project;
179
+ source-level markers: taint tracking, import resolution,
180
+ │ qualified call chains, cross-file template analysis
178
181
  ├─ Repair Executor detect → plan → fix → validate → commit/rollback
179
- └─ Knowledge Base 31 domains, 140 rules, evidence chains
182
+ └─ Knowledge Base 31 domains, 148 rules, evidence chains
180
183
  ```
181
184
 
182
185
  ### Interfaces
@@ -191,12 +194,20 @@ SDK (src/sdk.ts) verify() → APPROVED / NEEDS_REVIEW / BLOCKED
191
194
 
192
195
  ---
193
196
 
197
+ ## Community & Feedback
198
+
199
+ <p align="center">
200
+ <img src="https://raw.githubusercontent.com/shenlian19831109/progmune-runtime/main/assets/wechat-group.png" width="240" alt="Progmune user discussion group QR code" />
201
+ </p>
202
+
203
+ Your feedback shapes Progmune. Scan the QR code to join the user discussion group (WeChat), or open a [GitHub Issue](https://github.com/shenlian19831109/progmune-runtime/issues) for bug reports, feature requests, and suggestions.
204
+
205
+ ---
206
+
194
207
  ## Scientific Foundation
195
208
 
196
209
  Progmune is built on the premise that **LLM outputs are statistical performances, not reasoning** — a view developed by Subbarao Kambhampati et al. in the position paper ["Stop Anthropomorphizing Intermediate Tokens as Reasoning/Thinking Traces!"](https://arxiv.org/abs/2505.22285) (arXiv:2505.22285, 2025) and elaborated in his ICML 2026 talk "On the Role of Verifiers and Thinking Traces in Reasoning Models". Rather than trusting what the model says about code, Progmune verifies what the program actually does — using protocol state machines, IR extraction, and evidence-backed decision chains.
197
210
 
198
- Coverage-gap analysis borrows the "two-hump problem" terminology **as a cross-domain analogy** from Sergei Gukov's work in mathematical physics (the Andrews-Curtis conjecture in group theory, 2026) — it describes a bimodal coverage distribution, not a collaboration. See [Two-Hump Report](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/two-hump-report.md) for the full methodology.
199
-
200
211
  → [Investor Whitepaper](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/Progmune_投资人白皮书_v2.0.html) · [Trust Decision Model](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/ai-trust-decision-model-v1.md)
201
212
 
202
213
  ---
package/README.zh-CN.md CHANGED
@@ -106,6 +106,8 @@ Progmune 对能验证什么、不能验证什么保持诚实。
106
106
  | **C** | ⚠️ 仅研究 | 黄金基准 F1=16.5%。L3 跨函数实验已终止;L4 无计划。见 [C 语言状态](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/c-language-status.md)。 |
107
107
  | **Go, Java** | ❌ 无 | 规划中 |
108
108
 
109
+ **多语言 IR(注册表式)。** TypeScript(ts-morph)与 Python(AST)提取器是同一注册表(`src/extract-project-ir.ts`)中的注册项;`extractProjectIR` 把检测到的所有语言合并为一份函数 IR,由 agent loop、`execute()` 与 MCP server 共享——agent 可在两种语言上编排函数协议链。新增语言(Go、Java……)= 注册一条提取器,调用方零改动。
110
+
109
111
  **框架适配器:2/13。** Express ✅ 与 tRPC ✅ 有专用检测器;Next.js 有版本感知治理;NestJS 部分支持。Django、FastAPI 及另外 8 个待适配——框架适配是 #1 产品缺口。
110
112
 
111
113
  ### Progmune 不覆盖什么(诚实边界)
@@ -160,7 +162,7 @@ C 分析**仅研究**:黄金基准 F1=16.5%,覆盖 4 个仓库(curl、libs
160
162
  - 打破引导死锁:全部 21 个协议命名空间已有规则词汇
161
163
  - `excludePatterns` + `languages` 架构管理误报
162
164
 
163
- → [双峰报告](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/two-hump-report.md) · [P0-P3 终报](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/p0-p3-final-report.md)
165
+ → [P0-P3 终报](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/p0-p3-final-report.md)
164
166
 
165
167
  ---
166
168
 
@@ -172,10 +174,11 @@ SDK (src/sdk.ts) verify() → APPROVED / NEEDS_REVIEW / BLOCKED
172
174
  ├─ 策略引擎 企业策略执行(ALLOW/WARN/BLOCK)
173
175
  ├─ SSG 校验器 协议状态机验证
174
176
  ├─ 协议检测器 基于正则的协议步骤检测(22 个检测器)
175
- ├─ IR 提取器 TypeScript(ts-morph)+ Python(ast 模块)→ 函数 IR;
177
+ ├─ IR 提取 注册表式:TS(ts-morph)+ Python(ast 模块)
178
+ │ 合并为每项目一份函数 IR;
176
179
  │ 源码级标记:污点追踪、import 解析、限定调用链、跨文件模板分析
177
180
  ├─ 修复执行器 detect → plan → fix → validate → commit/rollback
178
- └─ 知识库 31 个域、140 条规则、证据链
181
+ └─ 知识库 31 个域、148 条规则、证据链
179
182
  ```
180
183
 
181
184
  ### 接口
@@ -190,12 +193,20 @@ SDK (src/sdk.ts) verify() → APPROVED / NEEDS_REVIEW / BLOCKED
190
193
 
191
194
  ---
192
195
 
196
+ ## 社区与反馈
197
+
198
+ <p align="center">
199
+ <img src="https://raw.githubusercontent.com/shenlian19831109/progmune-runtime/main/assets/wechat-group.png" width="240" alt="Progmune 用户讨论群二维码" />
200
+ </p>
201
+
202
+ 你的意见塑造 Progmune。扫码加入用户讨论群(微信),或通过 [GitHub Issue](https://github.com/shenlian19831109/progmune-runtime/issues) 提交缺陷报告、功能需求与建议。
203
+
204
+ ---
205
+
193
206
  ## 科学基础
194
207
 
195
208
  Progmune 建立在"**LLM 输出是统计表演而非推理**"这一前提上——该观点源自 Subbarao Kambhampati 等人的立场论文 ["Stop Anthropomorphizing Intermediate Tokens as Reasoning/Thinking Traces!"](https://arxiv.org/abs/2505.22285)(arXiv:2505.22285,2025),并在其 ICML 2026 演讲 "On the Role of Verifiers and Thinking Traces in Reasoning Models" 中展开。Progmune 不信任模型对代码的说法,而是用协议状态机、IR 提取与证据链验证程序实际行为。
196
209
 
197
- 覆盖缺口分析借用"双峰问题"术语作为**跨域类比**,源自 Sergei Gukov 在数学物理领域的工作(群论中的 Andrews-Curtis 猜想,2026)——它描述的是双峰覆盖分布,而非合作关系。详见 [双峰报告](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/two-hump-report.md)。
198
-
199
210
  → [投资人白皮书](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/Progmune_投资人白皮书_v2.0.html) · [信任决策模型](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/ai-trust-decision-model-v1.md)
200
211
 
201
212
  ---
@@ -49,7 +49,7 @@ exports.extractIRWithDelta = extractIRWithDelta;
49
49
  const fs = __importStar(require("fs"));
50
50
  const path = __importStar(require("path"));
51
51
  const child_process_1 = require("child_process");
52
- const extract_ir_1 = require("./extract-ir");
52
+ const extract_project_ir_1 = require("./extract-project-ir");
53
53
  // ── Git Context ──
54
54
  function git(projectPath, args) {
55
55
  return (0, child_process_1.execSync)(`git -C "${projectPath}" ${args}`, {
@@ -125,7 +125,7 @@ function listSourceFiles(projectPath) {
125
125
  * prevNames 缺省时只返回全量 IR(delta 为空)。
126
126
  */
127
127
  function extractIRWithDelta(projectPath, prevNames) {
128
- const ir = (0, extract_ir_1.extractIR)(projectPath);
128
+ const ir = (0, extract_project_ir_1.extractProjectIR)(projectPath);
129
129
  const names = ir.map((f) => String(f.name || "")).filter(Boolean);
130
130
  if (!prevNames) {
131
131
  return { ir, delta: { added: [], removed: [], functionCount: names.length } };
@@ -41,16 +41,16 @@ var __importStar = (this && this.__importStar) || (function () {
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  const vitest_1 = require("vitest");
43
43
  const child_process_1 = require("child_process");
44
- const extract_ir_1 = require("./extract-ir");
44
+ const extract_project_ir_1 = require("./extract-project-ir");
45
45
  const agent_perception_1 = require("./agent-perception");
46
46
  vitest_1.vi.mock("child_process", () => ({
47
47
  execSync: vitest_1.vi.fn(),
48
48
  }));
49
- vitest_1.vi.mock("./extract-ir", () => ({
50
- extractIR: vitest_1.vi.fn(),
49
+ vitest_1.vi.mock("./extract-project-ir", () => ({
50
+ extractProjectIR: vitest_1.vi.fn(),
51
51
  }));
52
52
  const mockExecSync = vitest_1.vi.mocked(child_process_1.execSync);
53
- const mockExtractIR = vitest_1.vi.mocked(extract_ir_1.extractIR);
53
+ const mockExtractProjectIR = vitest_1.vi.mocked(extract_project_ir_1.extractProjectIR);
54
54
  (0, vitest_1.beforeEach)(() => {
55
55
  vitest_1.vi.clearAllMocks();
56
56
  });
@@ -82,7 +82,7 @@ const mockExtractIR = vitest_1.vi.mocked(extract_ir_1.extractIR);
82
82
  (0, vitest_1.expect)(ctx.error).toContain("not a git repository");
83
83
  });
84
84
  (0, vitest_1.it)("extractIRWithDelta 计算新增/消失函数差集", () => {
85
- mockExtractIR.mockReturnValue([
85
+ mockExtractProjectIR.mockReturnValue([
86
86
  { name: "verify_password" },
87
87
  { name: "main" },
88
88
  ]);
@@ -69,8 +69,23 @@ function buildPath(startFn, rules, visited) {
69
69
  }
70
70
  /**
71
71
  * Run the bootstrap validation experiment.
72
+ *
73
+ * 无参调用结果缓存(纯计算、确定性):同进程内多次调用复用同一结果。
74
+ * 运行时与本地语料规模线性相关(语料丰富时可 >30s / 2GB 堆)——
75
+ * function-synonyms 等测试会 5 次重计算,缓存降为 1 次。
72
76
  */
77
+ let _defaultResultCache = null;
73
78
  async function runBootstrapValidation(existingRules, extraSequences) {
79
+ if (!existingRules && !extraSequences) {
80
+ if (!_defaultResultCache) {
81
+ _defaultResultCache = runBootstrapValidationUncached();
82
+ }
83
+ return _defaultResultCache;
84
+ }
85
+ return runBootstrapValidationUncached(existingRules, extraSequences);
86
+ }
87
+ /** 原实现(无缓存)。 */
88
+ async function runBootstrapValidationUncached(existingRules, extraSequences) {
74
89
  // 1. Ground truth
75
90
  const defs = (0, protocol_coverage_1.loadDefaultProtocolDefinitions)();
76
91
  const originalRules = existingRules || new Map();
package/dist/execute.js CHANGED
@@ -49,7 +49,7 @@ exports.verifyFileMarker = verifyFileMarker;
49
49
  const fs = __importStar(require("fs"));
50
50
  const path = __importStar(require("path"));
51
51
  const planner_1 = require("./planner");
52
- const extract_ir_1 = require("./extract-ir");
52
+ const extract_project_ir_1 = require("./extract-project-ir");
53
53
  const failure_collector_1 = require("./failure-collector");
54
54
  const emitter_1 = require("./emitter");
55
55
  const METRICS_FILE = ".progmune_corpus/metrics.json";
@@ -101,7 +101,7 @@ async function execute(intent, projectPath, filePath) {
101
101
  // 1. IR extraction
102
102
  let ir;
103
103
  try {
104
- ir = (0, extract_ir_1.extractIR)(projectPath);
104
+ ir = (0, extract_project_ir_1.extractProjectIR)(projectPath);
105
105
  }
106
106
  catch (e) {
107
107
  return { success: false, degraded: false, code: "", sessionId: "", hash: "", ruleHash: "", irFunctionCount: 0, protocolRuleCount: 0, violations: 0, repairApplied: false, repairCount: 0, repairBranchIds: [], branchWinner: undefined, error: `IR extraction failed: ${e.message}` };
@@ -50,10 +50,18 @@ exports.extractIRPython = extractIRPython;
50
50
  exports.isPythonProject = isPythonProject;
51
51
  const child_process_1 = require("child_process");
52
52
  const fs = __importStar(require("fs"));
53
+ const os = __importStar(require("os"));
53
54
  const path = __importStar(require("path"));
54
- function extractIRPython(projectRoot) {
55
+ /**
56
+ * @param projectRoot - Project root to scan
57
+ * @param outPath - Output path for the Python script. Defaults to a temp
58
+ * file (cleaned up afterwards) so extraction never clobbers the project's
59
+ * own ir.json — the merged extractProjectIR runs this on TS projects too.
60
+ * Pass an explicit path when the extracted IR must be persisted.
61
+ */
62
+ function extractIRPython(projectRoot, outPath) {
55
63
  const scriptPath = path.resolve(__dirname, "..", "tools", "extract_ir.py");
56
- const irPath = path.resolve(projectRoot, "ir.json");
64
+ const irPath = outPath ?? path.join(os.tmpdir(), `progmune-py-ir-${process.pid}-${Date.now()}.json`);
57
65
  const cmd = `python3 "${scriptPath}" "${projectRoot}" "${irPath}"`;
58
66
  try {
59
67
  (0, child_process_1.execSync)(cmd, { encoding: "utf-8", stdio: "pipe" });
@@ -64,7 +72,18 @@ function extractIRPython(projectRoot) {
64
72
  }
65
73
  if (!fs.existsSync(irPath))
66
74
  return [];
67
- const raw = JSON.parse(fs.readFileSync(irPath, "utf-8"));
75
+ let raw;
76
+ try {
77
+ raw = JSON.parse(fs.readFileSync(irPath, "utf-8"));
78
+ }
79
+ finally {
80
+ if (!outPath) {
81
+ try {
82
+ fs.unlinkSync(irPath);
83
+ }
84
+ catch { /* 临时文件清理失败无害 */ }
85
+ }
86
+ }
68
87
  if (!Array.isArray(raw))
69
88
  return [];
70
89
  // Map Python IR records to FunctionInfo
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ /**
3
+ * Project IR extraction — merged multi-language entry point.
4
+ *
5
+ * Registry pattern: each supported language contributes a detector +
6
+ * extractor entry. extractProjectIR runs every extractor whose language
7
+ * is detected in the project and merges the results into one FunctionInfo
8
+ * list, so TS + Python coexist in a mixed project.
9
+ *
10
+ * Adding a language later (Go, Java, ...) = register one entry in
11
+ * LANGUAGE_EXTRACTORS. The agent loop (extractIRWithDelta), execute()'s
12
+ * ir.json write and the MCP server all pick it up automatically — no
13
+ * dispatch rewiring anywhere else.
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.LANGUAGE_EXTRACTORS = void 0;
50
+ exports.detectLanguages = detectLanguages;
51
+ exports.extractProjectIR = extractProjectIR;
52
+ const fs = __importStar(require("fs"));
53
+ const path = __importStar(require("path"));
54
+ const extract_ir_1 = require("./extract-ir");
55
+ const extract_ir_python_1 = require("./extract-ir-python");
56
+ const SKIP_DIRS = new Set([
57
+ "node_modules", "dist", "build", ".git", ".progmune_corpus",
58
+ "__pycache__", "venv", ".venv",
59
+ ]);
60
+ /** 有界递归扫描:项目是否含指定扩展名源文件(首个命中即返回)。 */
61
+ function hasSourceFiles(projectRoot, exts) {
62
+ const stack = [projectRoot];
63
+ const seen = new Set();
64
+ while (stack.length > 0) {
65
+ const dir = stack.pop();
66
+ if (seen.has(dir))
67
+ continue;
68
+ seen.add(dir);
69
+ let entries;
70
+ try {
71
+ entries = fs.readdirSync(dir, { withFileTypes: true });
72
+ }
73
+ catch {
74
+ continue;
75
+ }
76
+ for (const e of entries) {
77
+ const full = path.join(dir, e.name);
78
+ if (e.isDirectory()) {
79
+ if (!SKIP_DIRS.has(e.name) && !e.name.startsWith("."))
80
+ stack.push(full);
81
+ }
82
+ else if (exts.has(path.extname(e.name))) {
83
+ return true;
84
+ }
85
+ }
86
+ }
87
+ return false;
88
+ }
89
+ /** 已注册语言提取器(新增语言在此追加一条)。 */
90
+ exports.LANGUAGE_EXTRACTORS = [
91
+ {
92
+ language: "typescript",
93
+ detect: (p) => hasSourceFiles(p, new Set([".ts", ".tsx"])),
94
+ extract: (p) => (0, extract_ir_1.extractIR)(p),
95
+ },
96
+ {
97
+ language: "python",
98
+ detect: (p) => hasSourceFiles(p, new Set([".py"])),
99
+ extract: (p) => (0, extract_ir_python_1.extractIRPython)(p),
100
+ },
101
+ ];
102
+ /** 项目检测到的语言列表(审计/标签用)。 */
103
+ function detectLanguages(projectRoot) {
104
+ return exports.LANGUAGE_EXTRACTORS.filter((e) => e.detect(projectRoot)).map((e) => e.language);
105
+ }
106
+ /**
107
+ * Extract the merged project IR across all detected languages.
108
+ *
109
+ * 单语言提取失败不拖垮其余语言(与感知层 best-effort 原则一致);
110
+ * 仅当所有检测到的语言全部失败时才抛错,保留 execute 的硬失败语义。
111
+ *
112
+ * @param projectRoot - Absolute path to project root
113
+ * @param extractors - Registry override (tests inject fake entries here)
114
+ * @returns Merged FunctionInfo list
115
+ */
116
+ function extractProjectIR(projectRoot, extractors = exports.LANGUAGE_EXTRACTORS) {
117
+ const merged = [];
118
+ let anyDetected = false;
119
+ let anySucceeded = false;
120
+ for (const e of extractors) {
121
+ if (!e.detect(projectRoot))
122
+ continue;
123
+ anyDetected = true;
124
+ try {
125
+ merged.push(...e.extract(projectRoot));
126
+ anySucceeded = true;
127
+ }
128
+ catch (err) {
129
+ console.error(`[extractProjectIR] ${e.language} 提取失败: ${err?.message || err}`);
130
+ }
131
+ }
132
+ if (anyDetected && !anySucceeded) {
133
+ throw new Error("所有已检测语言的 IR 提取均失败");
134
+ }
135
+ return merged;
136
+ }
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ /**
3
+ * Project IR extraction (merged multi-language registry) — unit tests.
4
+ *
5
+ * Registry injection: tests pass fake extractor entries to extractProjectIR
6
+ * so no real extractor runs; the real detectors are covered by two
7
+ * temp-dir tests (per module convention, FS use is kept minimal).
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ const vitest_1 = require("vitest");
44
+ const fs = __importStar(require("fs"));
45
+ const os = __importStar(require("os"));
46
+ const path = __importStar(require("path"));
47
+ const extract_project_ir_1 = require("./extract-project-ir");
48
+ (0, vitest_1.describe)("extractProjectIR(注册表合并)", () => {
49
+ (0, vitest_1.it)("合并所有检测到语言的函数", () => {
50
+ const tsFns = [{ name: "getSession", file: "src/auth.ts" }];
51
+ const pyFns = [{ name: "verify_password", file: "auth_service.py" }];
52
+ const extractors = [
53
+ { language: "typescript", detect: () => true, extract: () => tsFns },
54
+ { language: "python", detect: () => true, extract: () => pyFns },
55
+ ];
56
+ const ir = (0, extract_project_ir_1.extractProjectIR)("/tmp/fake", extractors);
57
+ (0, vitest_1.expect)(ir).toEqual([...tsFns, ...pyFns]);
58
+ });
59
+ (0, vitest_1.it)("未检测到的语言不运行提取器", () => {
60
+ const ran = [];
61
+ const extractors = [
62
+ { language: "typescript", detect: () => true, extract: () => { ran.push("ts"); return []; } },
63
+ { language: "python", detect: () => false, extract: () => { ran.push("py"); return []; } },
64
+ ];
65
+ (0, extract_project_ir_1.extractProjectIR)("/tmp/fake", extractors);
66
+ (0, vitest_1.expect)(ran).toEqual(["ts"]);
67
+ });
68
+ (0, vitest_1.it)("单语言提取失败不拖垮其余语言", () => {
69
+ vitest_1.vi.spyOn(console, "error").mockImplementation(() => { });
70
+ const extractors = [
71
+ { language: "typescript", detect: () => true, extract: () => { throw new Error("broken tsconfig"); } },
72
+ { language: "python", detect: () => true, extract: () => [{ name: "f" }] },
73
+ ];
74
+ const ir = (0, extract_project_ir_1.extractProjectIR)("/tmp/fake", extractors);
75
+ (0, vitest_1.expect)(ir).toEqual([{ name: "f" }]);
76
+ });
77
+ (0, vitest_1.it)("所有检测到的语言都失败时抛错(保留 execute 硬失败语义)", () => {
78
+ vitest_1.vi.spyOn(console, "error").mockImplementation(() => { });
79
+ const extractors = [
80
+ { language: "typescript", detect: () => true, extract: () => { throw new Error("broken"); } },
81
+ ];
82
+ (0, vitest_1.expect)(() => (0, extract_project_ir_1.extractProjectIR)("/tmp/fake", extractors)).toThrow();
83
+ });
84
+ (0, vitest_1.it)("无任何检测到的语言时返回空数组,不抛错", () => {
85
+ const extractors = [
86
+ { language: "typescript", detect: () => false, extract: () => [] },
87
+ { language: "python", detect: () => false, extract: () => [] },
88
+ ];
89
+ (0, vitest_1.expect)((0, extract_project_ir_1.extractProjectIR)("/tmp/fake", extractors)).toEqual([]);
90
+ });
91
+ });
92
+ (0, vitest_1.describe)("detectLanguages(真实探测器)", () => {
93
+ (0, vitest_1.it)("TS/Python 混合项目两种语言都被检测", () => {
94
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-lang-"));
95
+ try {
96
+ fs.writeFileSync(path.join(dir, "main.ts"), "export function f() {}");
97
+ fs.mkdirSync(path.join(dir, "src"));
98
+ fs.writeFileSync(path.join(dir, "src", "app.py"), "def g():\n pass\n");
99
+ (0, vitest_1.expect)((0, extract_project_ir_1.detectLanguages)(dir).sort()).toEqual(["python", "typescript"]);
100
+ }
101
+ finally {
102
+ fs.rmSync(dir, { recursive: true, force: true });
103
+ }
104
+ });
105
+ (0, vitest_1.it)("依赖目录里的源文件不计入检测(node_modules/.py)", () => {
106
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-lang2-"));
107
+ try {
108
+ fs.mkdirSync(path.join(dir, "node_modules", "pkg"), { recursive: true });
109
+ fs.writeFileSync(path.join(dir, "node_modules", "pkg", "x.py"), "x = 1\n");
110
+ (0, vitest_1.expect)((0, extract_project_ir_1.detectLanguages)(dir)).toEqual([]);
111
+ }
112
+ finally {
113
+ fs.rmSync(dir, { recursive: true, force: true });
114
+ }
115
+ });
116
+ });
@@ -49,12 +49,16 @@ const bootstrap_validation_1 = require("./bootstrap-validation");
49
49
  });
50
50
  });
51
51
  (0, vitest_1.describe)("Synonym Normalization Impact", () => {
52
+ // 注:这两个测试的重度与本地语料规模线性相关(.progmune_corpus 864+ 条时
53
+ // 实测 40-130s)。CI 干净检出无语料 → 秒级通过;本地语料丰富 → 需放宽超时。
54
+ // 显式超时 180s,不依赖 vitest 全局 30s。
55
+ const CORPUS_HEAVY_TIMEOUT = 180000;
52
56
  (0, vitest_1.it)("reduces unique function count via normalization", async () => {
53
57
  const report = await (0, function_synonyms_1.runSynonymNormalization)();
54
58
  (0, vitest_1.expect)(report.uniqueAfter).toBeLessThanOrEqual(report.uniqueBefore);
55
59
  (0, vitest_1.expect)(report.uniqueAfter).toBeLessThanOrEqual(report.uniqueBefore);
56
60
  (0, function_synonyms_1.printSynonymReport)(report);
57
- });
61
+ }, CORPUS_HEAVY_TIMEOUT);
58
62
  (0, vitest_1.it)("bootstrap function overlap improves with normalization", async () => {
59
63
  // Baseline without normalization
60
64
  const baseline = await (0, bootstrap_validation_1.runBootstrapValidation)();
@@ -64,5 +68,5 @@ const bootstrap_validation_1 = require("./bootstrap-validation");
64
68
  console.log(`Function overlap: ${(after.functionOverlap * 100).toFixed(0)}%`);
65
69
  console.log(`State overlap: ${(after.stateOverlap * 100).toFixed(0)}%`);
66
70
  console.log(`Behavioral: ${after.behavioralMatch}/${after.behavioralTotal}`);
67
- }, 30000);
71
+ }, CORPUS_HEAVY_TIMEOUT);
68
72
  });
@@ -39,8 +39,7 @@ for (const envPath of [
39
39
  }
40
40
  }
41
41
  import { plan } from "./planner.js";
42
- import { extractIR } from "./extract-ir.js";
43
- import { extractIRPython, isPythonProject } from "./extract-ir-python.js";
42
+ import { extractProjectIR, detectLanguages } from "./extract-project-ir.js";
44
43
  import { emitCode } from "./emitter.js";
45
44
  import { recordRun } from "./feedback.js";
46
45
  import { reportFingerprints } from "./immune-reporter.js";
@@ -419,10 +418,9 @@ Then restart Claude Code.`,
419
418
  log.warn(`No .ts files found in project root, IR may be empty`);
420
419
  }
421
420
  // IR extraction — auto-detect language
422
- const isPython = isPythonProject(projectPath);
423
421
  let ir;
424
422
  try {
425
- ir = isPython ? extractIRPython(projectPath) : extractIR(projectPath);
423
+ ir = extractProjectIR(projectPath);
426
424
  }
427
425
  catch (e) {
428
426
  return {
@@ -573,9 +571,9 @@ Then restart Claude Code.`,
573
571
  const warn = (msg) => results.push(`! ${msg}`);
574
572
  // 1. IR extraction — auto-detect language
575
573
  try {
576
- const py = isPythonProject(projectPath);
577
- const ir = py ? extractIRPython(projectPath) : extractIR(projectPath);
578
- pass(`IR (${py ? "Python" : "TypeScript"}): ${ir.length} functions extracted`);
574
+ const langs = detectLanguages(projectPath);
575
+ const ir = extractProjectIR(projectPath);
576
+ pass(`IR (${langs.length > 0 ? langs.join("+") : "无已支持语言"}): ${ir.length} functions extracted`);
579
577
  }
580
578
  catch (e) {
581
579
  fail(`IR extraction: ${e.message}`);
package/dist/planner.js CHANGED
@@ -392,26 +392,10 @@ function validateProtocolWithTransitions(actions, protocols, namespaceInitialSta
392
392
  }
393
393
  }
394
394
  // End-of-sequence check: held resources must be released (resource leak).
395
- // A state S is resource-holding when some rule REQUIRES S and INVALIDATES S
396
- // (acquire/release semantics — e.g. FILE_OPEN set by open_file, released by
397
- // close_file). Only RESOURCE-LIFECYCLE namespaces apply: session/auth flows
398
- // legitimately END with an active session (SESSION_ACTIVE is not a leak).
399
- const RESOURCE_NS = /^(file|db|database|connection|conn|socket|stream|resource|io)/i;
400
- const heldStates = [];
401
- for (const p of protocols) {
402
- const ann = p.protocol;
403
- if (!ann)
404
- continue;
405
- const ns = ann.namespace || "";
406
- if (!RESOURCE_NS.test(ns))
407
- continue;
408
- const inv = ann.invalidate || [];
409
- const pre = ann.pre_states || [];
410
- for (const s of inv) {
411
- if (pre.includes(s))
412
- heldStates.push({ state: s, releaseFn: p.function, namespace: ns });
413
- }
414
- }
395
+ // 共享判定见 ssg-validator.findHeldResourceStates:资源生命周期命名空间 +
396
+ // pre/invalidate 交集(获取/释放语义)。会话/认证流合法地以活跃会话结束,
397
+ // 不在检查范围(SESSION_ACTIVE 不是泄漏)。
398
+ const heldStates = (0, ssg_validator_1.findHeldResourceStates)(rules);
415
399
  // 只检查"本序列中获取"的持有状态——继承自命名空间初始状态的
416
400
  // (如 db 初始即 DB_CONNECTED)不算泄漏。
417
401
  const acquiredStates = new Set();
@@ -33,7 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.StateMachineValidator = exports.InvariantViolationError = void 0;
36
+ exports.StateMachineValidator = exports.InvariantViolationError = exports.RESOURCE_NAMESPACE_RE = void 0;
37
+ exports.findHeldResourceStates = findHeldResourceStates;
37
38
  exports.rebuildState = rebuildState;
38
39
  exports.applyTransitionDelta = applyTransitionDelta;
39
40
  exports.findFixPathStatic = findFixPathStatic;
@@ -51,6 +52,28 @@ exports.explainRejection = explainRejection;
51
52
  exports.rejectionToJSON = rejectionToJSON;
52
53
  exports.parseProtocolsFromJSON = parseProtocolsFromJSON;
53
54
  const crypto = __importStar(require("crypto"));
55
+ /** 资源生命周期命名空间:会话/认证流合法地以活跃会话结束(SESSION_ACTIVE 不是泄漏),
56
+ * 只有资源类命名空间(文件/数据库/连接/流等)做序列末尾持有状态检查。 */
57
+ exports.RESOURCE_NAMESPACE_RE = /^(file|db|database|connection|conn|socket|stream|resource|io)/i;
58
+ /**
59
+ * 找出资源持有状态候选:某规则的 pre_states 与 invalidate 的交集
60
+ * (获取/释放语义——如 FILE_OPEN 由 open_file 设置、close_file 释放),
61
+ * 仅限资源生命周期命名空间。planner 与 trust 桥接共用(endState 检查)。
62
+ */
63
+ function findHeldResourceStates(rules) {
64
+ const held = [];
65
+ for (const [fn, ann] of rules) {
66
+ const ns = ann.namespace || "";
67
+ if (!exports.RESOURCE_NAMESPACE_RE.test(ns))
68
+ continue;
69
+ for (const s of ann.invalidate || []) {
70
+ if ((ann.pre_states || []).includes(s)) {
71
+ held.push({ state: s, releaseFn: fn, namespace: ns });
72
+ }
73
+ }
74
+ }
75
+ return held;
76
+ }
54
77
  const DEFAULT_NAMESPACE = "_global";
55
78
  class InvariantViolationError extends Error {
56
79
  constructor(message, detail) {
@@ -342,6 +342,18 @@ function validateSequenceWithSSG(steps, rules, namespaceInitialStates, file, ali
342
342
  return undefined;
343
343
  }
344
344
  })();
345
+ // 本序列中"新获取"的资源状态(endState 检查只针对本序列获取、未释放的状态——
346
+ // 继承自命名空间初始状态的不算泄漏,与 planner 语义一致)
347
+ const acquiredStates = new Set();
348
+ const trackAcquiredStates = (before, after) => {
349
+ for (const ns of Object.keys(after)) {
350
+ const prev = before?.[ns] || [];
351
+ for (const s of after[ns]) {
352
+ if (!prev.includes(s))
353
+ acquiredStates.add(`${ns}::${s}`);
354
+ }
355
+ }
356
+ };
345
357
  for (let i = 0; i < steps.length; i++) {
346
358
  const step = steps[i];
347
359
  const namespace = DOMAIN_TO_NAMESPACE[step.domain] || "stateless";
@@ -385,6 +397,7 @@ function validateSequenceWithSSG(steps, rules, namespaceInitialStates, file, ali
385
397
  // Advance state
386
398
  ctx.ledger.push(result.transition);
387
399
  ctx.currentState = result.transition.statesAfter;
400
+ trackAcquiredStates(result.transition.statesBefore, result.transition.statesAfter);
388
401
  }
389
402
  else {
390
403
  violatedCalls++;
@@ -414,6 +427,7 @@ function validateSequenceWithSSG(steps, rules, namespaceInitialStates, file, ali
414
427
  // Still advance state on rejection (best-effort: apply transition anyway
415
428
  // so subsequent calls can be validated)
416
429
  ctx.ledger.push(result.transition);
430
+ trackAcquiredStates(result.transition.statesBefore, result.transition.statesAfter);
417
431
  }
418
432
  }
419
433
  catch {
@@ -440,6 +454,39 @@ function validateSequenceWithSSG(steps, rules, namespaceInitialStates, file, ali
440
454
  valid: true,
441
455
  });
442
456
  }
457
+ // ── End-of-sequence check: held resources must be released (resource leak).
458
+ // 共享判定见 ssg-validator.findHeldResourceStates:仅资源生命周期命名空间 +
459
+ // pre/invalidate 交集;只报本序列获取且最终仍持有的状态(对齐 planner)。
460
+ for (const hs of (0, ssg_validator_1.findHeldResourceStates)(rules)) {
461
+ const ctx = contexts.get(hs.namespace);
462
+ if (!ctx)
463
+ continue;
464
+ if (!acquiredStates.has(`${hs.namespace}::${hs.state}`))
465
+ continue;
466
+ const cur = ctx.currentState[hs.namespace] || [];
467
+ if (!cur.includes(hs.state))
468
+ continue;
469
+ violatedCalls++;
470
+ violations.push({
471
+ callName: "(end-of-sequence)",
472
+ namespace: hs.namespace,
473
+ currentState: cur,
474
+ requiredState: [],
475
+ fixPath: [hs.releaseFn],
476
+ matchedRule: hs.releaseFn,
477
+ endState: true,
478
+ explanation: `SSG end-state violation: resource state [${hs.state}] ` +
479
+ `acquired in this function is still held at end of sequence ` +
480
+ `(namespace ${hs.namespace}, current [${cur.join(", ")}]) — ` +
481
+ `missing release call: ${hs.releaseFn}.`,
482
+ });
483
+ trace.push({
484
+ call: "(end-of-sequence)",
485
+ namespace: hs.namespace,
486
+ matchedRule: hs.releaseFn,
487
+ valid: false,
488
+ });
489
+ }
443
490
  return {
444
491
  passed: violations.length === 0,
445
492
  trace,
@@ -459,19 +506,30 @@ function validateSequenceWithSSG(steps, rules, namespaceInitialStates, file, ali
459
506
  function ssgViolationsToTrustViolations(ssgResult, file, funcName) {
460
507
  return ssgResult.violations.map((v) => ({
461
508
  severity: "medium",
462
- rule_id: `SSG_${v.namespace.toUpperCase()}_STATE_VIOLATION`,
509
+ rule_id: v.endState
510
+ ? `SSG_${v.namespace.toUpperCase()}_END_STATE_VIOLATION`
511
+ : `SSG_${v.namespace.toUpperCase()}_STATE_VIOLATION`,
463
512
  file,
464
513
  function: funcName,
465
514
  message: v.explanation,
466
- evidence: `Call: ${v.callName} | Rule: ${v.matchedRule || "unknown"} | ` +
467
- `Required: [${v.requiredState.join(", ")}] | ` +
468
- `Current: [${v.currentState.join(", ")}]`,
469
- why: `Protocol state machine violation in namespace "${v.namespace}": ` +
470
- `function "${v.callName}" cannot be called in current state ` +
471
- `[${v.currentState.join(", ")}]. Required pre-states: [${v.requiredState.join(", ")}].`,
472
- fix: v.fixPath.length > 0
473
- ? `Insert before the violating call: ${v.fixPath.join(" ")}`
474
- : `Review protocol documentation for namespace "${v.namespace}" to understand required state transitions.`,
515
+ evidence: v.endState
516
+ ? `End-of-sequence: held resource state [${v.currentState.join(", ")}] | ` +
517
+ `Release call: ${v.fixPath.join(" ") || "unknown"}`
518
+ : `Call: ${v.callName} | Rule: ${v.matchedRule || "unknown"} | ` +
519
+ `Required: [${v.requiredState.join(", ")}] | ` +
520
+ `Current: [${v.currentState.join(", ")}]`,
521
+ why: v.endState
522
+ ? `Protocol state machine violation in namespace "${v.namespace}": ` +
523
+ `a resource acquired in this function ([${v.currentState.join(", ")}]) ` +
524
+ `is still held when the function ends — resource leak.`
525
+ : `Protocol state machine violation in namespace "${v.namespace}": ` +
526
+ `function "${v.callName}" cannot be called in current state ` +
527
+ `[${v.currentState.join(", ")}]. Required pre-states: [${v.requiredState.join(", ")}].`,
528
+ fix: v.endState
529
+ ? `Append the release call at the end of the function: ${v.fixPath.join(" → ")}`
530
+ : v.fixPath.length > 0
531
+ ? `Insert before the violating call: ${v.fixPath.join(" → ")}`
532
+ : `Review protocol documentation for namespace "${v.namespace}" to understand required state transitions.`,
475
533
  policy_ref: `protocol-safety.ssg.${v.namespace}`,
476
534
  }));
477
535
  }
@@ -167,17 +167,22 @@ function step(api, domain, description = "") {
167
167
  }
168
168
  });
169
169
  (0, vitest_1.it)("detects missing cleanup (file opened but not closed)", () => {
170
- // open_file without close_file — state machine will show FILE_OPEN still active
170
+ // open_file without close_file — held FILE_OPEN at end of sequence
171
171
  const steps = [
172
172
  step("fs.openSync", "conn_mgmt", "Open file"),
173
173
  step("fs.readFileSync", "conn_mgmt", "Read file contents"),
174
174
  // Missing: close_file
175
175
  ];
176
176
  const result = validate(steps);
177
- // The SSG state machine tracks that file is still open.
178
- // While it may not report this as a violation (since close_file
179
- // isn't called), the state trace should show FILE_OPEN is active.
180
- (0, vitest_1.expect)(result.trace.length).toBe(steps.length);
177
+ // endState 检查:序列末尾资源未释放 违规 + 追加式修复路径
178
+ const endState = result.violations.filter((v) => v.endState);
179
+ (0, vitest_1.expect)(endState).toHaveLength(1);
180
+ (0, vitest_1.expect)(endState[0].namespace).toBe("file");
181
+ (0, vitest_1.expect)(endState[0].currentState).toContain("FILE_OPEN");
182
+ (0, vitest_1.expect)(endState[0].fixPath).toEqual(["close_file"]);
183
+ (0, vitest_1.expect)(result.passed).toBe(false);
184
+ // trace 含步骤节点 + 末尾 endState 节点
185
+ (0, vitest_1.expect)(result.trace.length).toBe(steps.length + 1);
181
186
  });
182
187
  });
183
188
  (0, vitest_1.describe)("violation → TrustViolation conversion", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.4.1",
3
+ "version": "3.6.0",
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/",