progmune-runtime 3.3.8 → 3.4.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 +34 -0
- package/dist/agent-cli.js +209 -0
- package/dist/agent-loop.js +324 -0
- package/dist/agent-loop.test.js +256 -0
- package/dist/agent-patrol.js +200 -0
- package/dist/agent-patrol.test.js +141 -0
- package/dist/agent-perception.js +197 -0
- package/dist/agent-perception.test.js +125 -0
- package/dist/agent-supervision.js +145 -0
- package/dist/agent-supervision.test.js +60 -0
- package/dist/execute.js +6 -2
- package/dist/extract-ir.js +3 -1
- package/dist/patrol-cli.js +130 -0
- package/dist/planner-prompts.js +3 -0
- package/dist/planner.js +2 -0
- package/dist/trust/engine.js +81 -0
- package/package.json +4 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 12: Agent 感知层 (P2)
|
|
4
|
+
*
|
|
5
|
+
* 为 agent loop 提供"看清世界"的能力(设计文档 P2):
|
|
6
|
+
* - GitContext — 仓库扫描 + git 上下文(分支/最近提交/变更文件/源文件清单)
|
|
7
|
+
* - extractIRWithDelta — IR 提取 + 前后函数名差集(IR 增量重提的观测面)
|
|
8
|
+
* - RepoWatcher — 文件变更监听(fs.watch + 防抖)→ 触发 IR 重提回调
|
|
9
|
+
*
|
|
10
|
+
* 原则:感知失败不阻塞主循环(best-effort,明确记录 unavailable)。
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.RepoWatcher = void 0;
|
|
47
|
+
exports.collectGitContext = collectGitContext;
|
|
48
|
+
exports.extractIRWithDelta = extractIRWithDelta;
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const child_process_1 = require("child_process");
|
|
52
|
+
const extract_ir_1 = require("./extract-ir");
|
|
53
|
+
// ── Git Context ──
|
|
54
|
+
function git(projectPath, args) {
|
|
55
|
+
return (0, child_process_1.execSync)(`git -C "${projectPath}" ${args}`, {
|
|
56
|
+
encoding: "utf-8",
|
|
57
|
+
timeout: 10000,
|
|
58
|
+
stdio: "pipe",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** 收集仓库上下文(分支、最近提交、变更文件)与源文件清单。best-effort。 */
|
|
62
|
+
function collectGitContext(projectPath) {
|
|
63
|
+
try {
|
|
64
|
+
const branch = git(projectPath, "rev-parse --abbrev-ref HEAD").trim();
|
|
65
|
+
const recentCommits = git(projectPath, "log --oneline -5")
|
|
66
|
+
.split("\n").filter(Boolean);
|
|
67
|
+
// porcelain 输出首字符可能是空格(未暂存),必须整串不 trim 才能保住 3 字符状态列
|
|
68
|
+
const changedFiles = git(projectPath, "status --porcelain")
|
|
69
|
+
.split("\n").filter(Boolean).map((l) => l.slice(3).trim());
|
|
70
|
+
const sourceFiles = listSourceFiles(projectPath);
|
|
71
|
+
return { available: true, branch, recentCommits, changedFiles, sourceFiles };
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
// 非 git 仓库等 → 只给源文件清单
|
|
75
|
+
try {
|
|
76
|
+
return {
|
|
77
|
+
available: false,
|
|
78
|
+
branch: "",
|
|
79
|
+
recentCommits: [],
|
|
80
|
+
changedFiles: [],
|
|
81
|
+
sourceFiles: listSourceFiles(projectPath),
|
|
82
|
+
error: e?.message || String(e),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return { available: false, branch: "", recentCommits: [], changedFiles: [], sourceFiles: [], error: e?.message || String(e) };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** 浅层扫描项目源文件(不递归进依赖目录)。 */
|
|
91
|
+
function listSourceFiles(projectPath) {
|
|
92
|
+
const SKIP_DIRS = new Set(["node_modules", "dist", "build", ".git", ".progmune_corpus", "__pycache__", "venv", ".venv"]);
|
|
93
|
+
const EXTS = new Set([".ts", ".js", ".py", ".tsx", ".jsx"]);
|
|
94
|
+
const out = [];
|
|
95
|
+
const stack = [projectPath];
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
while (stack.length > 0 && out.length < 500) {
|
|
98
|
+
const dir = stack.pop();
|
|
99
|
+
if (seen.has(dir))
|
|
100
|
+
continue;
|
|
101
|
+
seen.add(dir);
|
|
102
|
+
let entries;
|
|
103
|
+
try {
|
|
104
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
for (const e of entries) {
|
|
110
|
+
const full = path.join(dir, e.name);
|
|
111
|
+
if (e.isDirectory()) {
|
|
112
|
+
if (!SKIP_DIRS.has(e.name) && !e.name.startsWith("."))
|
|
113
|
+
stack.push(full);
|
|
114
|
+
}
|
|
115
|
+
else if (EXTS.has(path.extname(e.name))) {
|
|
116
|
+
out.push(path.relative(projectPath, full));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out.sort();
|
|
121
|
+
}
|
|
122
|
+
// ── IR 增量 ──
|
|
123
|
+
/**
|
|
124
|
+
* 提取 IR 并计算与上次函数名集合的差集。
|
|
125
|
+
* prevNames 缺省时只返回全量 IR(delta 为空)。
|
|
126
|
+
*/
|
|
127
|
+
function extractIRWithDelta(projectPath, prevNames) {
|
|
128
|
+
const ir = (0, extract_ir_1.extractIR)(projectPath);
|
|
129
|
+
const names = ir.map((f) => String(f.name || "")).filter(Boolean);
|
|
130
|
+
if (!prevNames) {
|
|
131
|
+
return { ir, delta: { added: [], removed: [], functionCount: names.length } };
|
|
132
|
+
}
|
|
133
|
+
const cur = new Set(names);
|
|
134
|
+
const added = names.filter((n) => !prevNames.has(n));
|
|
135
|
+
const removed = [...prevNames].filter((n) => !cur.has(n));
|
|
136
|
+
return { ir, delta: { added, removed, functionCount: names.length } };
|
|
137
|
+
}
|
|
138
|
+
// ── Repo Watcher ──
|
|
139
|
+
const WATCH_EXTS = new Set([".ts", ".js", ".py", ".tsx", ".jsx"]);
|
|
140
|
+
/**
|
|
141
|
+
* 文件变更监听器:fs.watch 递归监听项目目录,按文件防抖后回调。
|
|
142
|
+
* 用途:agent 或用户修改文件后触发 IR 增量重提(IR_STALE 消费方)。
|
|
143
|
+
*/
|
|
144
|
+
class RepoWatcher {
|
|
145
|
+
constructor(projectPath, onChange, debounceMs = 500) {
|
|
146
|
+
this.projectPath = projectPath;
|
|
147
|
+
this.onChange = onChange;
|
|
148
|
+
this.debounceMs = debounceMs;
|
|
149
|
+
this.watcher = null;
|
|
150
|
+
this.timers = new Map();
|
|
151
|
+
}
|
|
152
|
+
start() {
|
|
153
|
+
if (this.watcher)
|
|
154
|
+
return;
|
|
155
|
+
try {
|
|
156
|
+
this.watcher = fs.watch(this.projectPath, { recursive: true }, (event, filename) => {
|
|
157
|
+
if (!filename)
|
|
158
|
+
return;
|
|
159
|
+
const ext = path.extname(String(filename));
|
|
160
|
+
if (!WATCH_EXTS.has(ext))
|
|
161
|
+
return;
|
|
162
|
+
const rel = path.relative(this.projectPath, path.join(this.projectPath, String(filename)));
|
|
163
|
+
// 防抖:同一文件的连续事件合并
|
|
164
|
+
const prev = this.timers.get(rel);
|
|
165
|
+
if (prev)
|
|
166
|
+
clearTimeout(prev);
|
|
167
|
+
this.timers.set(rel, setTimeout(() => {
|
|
168
|
+
this.timers.delete(rel);
|
|
169
|
+
try {
|
|
170
|
+
this.onChange(rel);
|
|
171
|
+
}
|
|
172
|
+
catch { /* 回调异常不杀死 watcher */ }
|
|
173
|
+
}, this.debounceMs));
|
|
174
|
+
});
|
|
175
|
+
this.watcher.on("error", () => { });
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
this.watcher = null; // recursive 不支持时降级为不可用
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
stop() {
|
|
182
|
+
for (const t of this.timers.values())
|
|
183
|
+
clearTimeout(t);
|
|
184
|
+
this.timers.clear();
|
|
185
|
+
if (this.watcher) {
|
|
186
|
+
try {
|
|
187
|
+
this.watcher.close();
|
|
188
|
+
}
|
|
189
|
+
catch { /* ignore */ }
|
|
190
|
+
this.watcher = null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
get active() {
|
|
194
|
+
return this.watcher !== null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
exports.RepoWatcher = RepoWatcher;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 12: 感知层测试 (P2)
|
|
4
|
+
*
|
|
5
|
+
* collectGitContext / extractIRWithDelta:mock git 与 IR,不触真实仓库。
|
|
6
|
+
* RepoWatcher:真实临时目录(fs.watch 需要真实文件系统)。
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
const vitest_1 = require("vitest");
|
|
43
|
+
const child_process_1 = require("child_process");
|
|
44
|
+
const extract_ir_1 = require("./extract-ir");
|
|
45
|
+
const agent_perception_1 = require("./agent-perception");
|
|
46
|
+
vitest_1.vi.mock("child_process", () => ({
|
|
47
|
+
execSync: vitest_1.vi.fn(),
|
|
48
|
+
}));
|
|
49
|
+
vitest_1.vi.mock("./extract-ir", () => ({
|
|
50
|
+
extractIR: vitest_1.vi.fn(),
|
|
51
|
+
}));
|
|
52
|
+
const mockExecSync = vitest_1.vi.mocked(child_process_1.execSync);
|
|
53
|
+
const mockExtractIR = vitest_1.vi.mocked(extract_ir_1.extractIR);
|
|
54
|
+
(0, vitest_1.beforeEach)(() => {
|
|
55
|
+
vitest_1.vi.clearAllMocks();
|
|
56
|
+
});
|
|
57
|
+
(0, vitest_1.describe)("agent-perception", () => {
|
|
58
|
+
(0, vitest_1.it)("collectGitContext 解析分支/提交/变更文件/源文件清单", () => {
|
|
59
|
+
mockExecSync.mockImplementation((cmd) => {
|
|
60
|
+
const c = String(cmd);
|
|
61
|
+
if (c.includes("rev-parse"))
|
|
62
|
+
return "main";
|
|
63
|
+
if (c.includes("log --oneline"))
|
|
64
|
+
return "abc123 feat: login\nbcd456 fix: session";
|
|
65
|
+
if (c.includes("status --porcelain"))
|
|
66
|
+
return " M src/auth.ts\n?? src/new.ts";
|
|
67
|
+
throw new Error("unexpected cmd: " + c);
|
|
68
|
+
});
|
|
69
|
+
const ctx = (0, agent_perception_1.collectGitContext)("/tmp/fake-project");
|
|
70
|
+
(0, vitest_1.expect)(ctx.available).toBe(true);
|
|
71
|
+
(0, vitest_1.expect)(ctx.branch).toBe("main");
|
|
72
|
+
(0, vitest_1.expect)(ctx.recentCommits).toHaveLength(2);
|
|
73
|
+
(0, vitest_1.expect)(ctx.changedFiles).toEqual(["src/auth.ts", "src/new.ts"]);
|
|
74
|
+
(0, vitest_1.expect)(ctx.sourceFiles.length).toBeGreaterThanOrEqual(0);
|
|
75
|
+
});
|
|
76
|
+
(0, vitest_1.it)("collectGitContext 非 git 仓库时降级为 available=false 且不抛", () => {
|
|
77
|
+
mockExecSync.mockImplementation(() => {
|
|
78
|
+
throw new Error("fatal: not a git repository");
|
|
79
|
+
});
|
|
80
|
+
const ctx = (0, agent_perception_1.collectGitContext)("/tmp/fake-project");
|
|
81
|
+
(0, vitest_1.expect)(ctx.available).toBe(false);
|
|
82
|
+
(0, vitest_1.expect)(ctx.error).toContain("not a git repository");
|
|
83
|
+
});
|
|
84
|
+
(0, vitest_1.it)("extractIRWithDelta 计算新增/消失函数差集", () => {
|
|
85
|
+
mockExtractIR.mockReturnValue([
|
|
86
|
+
{ name: "verify_password" },
|
|
87
|
+
{ name: "main" },
|
|
88
|
+
]);
|
|
89
|
+
const prev = new Set(["verify_password", "logout"]);
|
|
90
|
+
const { delta } = (0, agent_perception_1.extractIRWithDelta)("/tmp/fake-project", prev);
|
|
91
|
+
(0, vitest_1.expect)(delta.added).toEqual(["main"]);
|
|
92
|
+
(0, vitest_1.expect)(delta.removed).toEqual(["logout"]);
|
|
93
|
+
(0, vitest_1.expect)(delta.functionCount).toBe(2);
|
|
94
|
+
});
|
|
95
|
+
(0, vitest_1.it)("RepoWatcher 文件变更防抖回调", async () => {
|
|
96
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
97
|
+
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
98
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
99
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-watch-"));
|
|
100
|
+
const changed = [];
|
|
101
|
+
const w = new agent_perception_1.RepoWatcher(dir, (f) => changed.push(f), 50);
|
|
102
|
+
w.start();
|
|
103
|
+
// 等待 watcher 就绪后写文件
|
|
104
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
105
|
+
fs.writeFileSync(path.join(dir, "new.ts"), "export function f() {}");
|
|
106
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
107
|
+
w.stop();
|
|
108
|
+
(0, vitest_1.expect)(changed).toContain("new.ts");
|
|
109
|
+
(0, vitest_1.expect)(w.active).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
(0, vitest_1.it)("RepoWatcher 忽略非源文件扩展名", async () => {
|
|
112
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
113
|
+
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
114
|
+
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
115
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-watch2-"));
|
|
116
|
+
const changed = [];
|
|
117
|
+
const w = new agent_perception_1.RepoWatcher(dir, (f) => changed.push(f), 50);
|
|
118
|
+
w.start();
|
|
119
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
120
|
+
fs.writeFileSync(path.join(dir, "notes.txt"), "hello");
|
|
121
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
122
|
+
w.stop();
|
|
123
|
+
(0, vitest_1.expect)(changed).toHaveLength(0);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 12: Agent 自监督层 (P3)
|
|
4
|
+
*
|
|
5
|
+
* 运行项目测试并提取失败信息 —— 失败注入下一次尝试的 prompt(失败→prompt 回路)。
|
|
6
|
+
* 设计文档 P3:编译/测试失败反馈注入重试。
|
|
7
|
+
*
|
|
8
|
+
* 自动探测顺序:
|
|
9
|
+
* 1. package.json 有 "test" script → npm test --silent
|
|
10
|
+
* 2. 存在 .py 文件 → python3 -m pytest -q
|
|
11
|
+
* 3. 都没有 → { ran: false }(调用方跳过该门)
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.runProjectTests = runProjectTests;
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const child_process_1 = require("child_process");
|
|
51
|
+
// ── Helpers ──
|
|
52
|
+
const FAILURE_PATTERN = /(FAIL|✕|×|failed|Error:|error TS|AssertionError|FAILED)/i;
|
|
53
|
+
function extractFailures(output) {
|
|
54
|
+
return output
|
|
55
|
+
.split("\n")
|
|
56
|
+
.map((l) => l.trim())
|
|
57
|
+
.filter((l) => l.length > 0 && FAILURE_PATTERN.test(l))
|
|
58
|
+
.slice(0, 10);
|
|
59
|
+
}
|
|
60
|
+
function runCommand(cwd, command, timeoutMs) {
|
|
61
|
+
try {
|
|
62
|
+
const output = (0, child_process_1.execSync)(command, {
|
|
63
|
+
cwd,
|
|
64
|
+
timeout: timeoutMs,
|
|
65
|
+
encoding: "utf-8",
|
|
66
|
+
stdio: "pipe",
|
|
67
|
+
});
|
|
68
|
+
return { pass: true, output };
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
// 非零退出或超时 → 捕获输出
|
|
72
|
+
const output = `${e.stdout || ""}\n${e.stderr || ""}`;
|
|
73
|
+
return { pass: false, output, error: e?.message || String(e) };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ── Main ──
|
|
77
|
+
/**
|
|
78
|
+
* 自动探测并运行项目测试。
|
|
79
|
+
*
|
|
80
|
+
* @requires PROJECT_PATH @produces TEST_RESULT
|
|
81
|
+
*/
|
|
82
|
+
function runProjectTests(projectPath, timeoutMs = 60000) {
|
|
83
|
+
// 1) npm test
|
|
84
|
+
const pkgPath = path.join(projectPath, "package.json");
|
|
85
|
+
if (fs.existsSync(pkgPath)) {
|
|
86
|
+
try {
|
|
87
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
88
|
+
if (pkg.scripts?.test) {
|
|
89
|
+
const command = "npm test --silent";
|
|
90
|
+
const r = runCommand(projectPath, command, timeoutMs);
|
|
91
|
+
return {
|
|
92
|
+
ran: true,
|
|
93
|
+
pass: r.pass,
|
|
94
|
+
failures: extractFailures(r.output),
|
|
95
|
+
command,
|
|
96
|
+
error: r.error,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch { /* package.json 解析失败 → 继续探测 */ }
|
|
101
|
+
}
|
|
102
|
+
// 2) pytest
|
|
103
|
+
const hasPy = listQuickly(projectPath, (e) => e.endsWith(".py"));
|
|
104
|
+
if (hasPy) {
|
|
105
|
+
const command = "python3 -m pytest -q";
|
|
106
|
+
const r = runCommand(projectPath, command, timeoutMs);
|
|
107
|
+
if (r.error && /no module named pytest/i.test(r.error + r.output)) {
|
|
108
|
+
return { ran: false, pass: true, failures: [], command, error: "pytest 未安装" };
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
ran: true,
|
|
112
|
+
pass: r.pass,
|
|
113
|
+
failures: extractFailures(r.output),
|
|
114
|
+
command,
|
|
115
|
+
error: r.error,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return { ran: false, pass: true, failures: [], command: "(无测试脚本)" };
|
|
119
|
+
}
|
|
120
|
+
/** 浅层探测是否存在匹配文件(不递归依赖目录)。 */
|
|
121
|
+
function listQuickly(projectPath, match) {
|
|
122
|
+
const SKIP = new Set(["node_modules", "dist", "build", ".git", "__pycache__", "venv", ".venv"]);
|
|
123
|
+
const stack = [projectPath];
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
while (stack.length > 0) {
|
|
126
|
+
const dir = stack.pop();
|
|
127
|
+
if (seen.has(dir))
|
|
128
|
+
continue;
|
|
129
|
+
seen.add(dir);
|
|
130
|
+
let entries;
|
|
131
|
+
try {
|
|
132
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
for (const e of entries) {
|
|
138
|
+
if (e.isFile() && match(e.name))
|
|
139
|
+
return true;
|
|
140
|
+
if (e.isDirectory() && !SKIP.has(e.name) && !e.name.startsWith("."))
|
|
141
|
+
stack.push(path.join(dir, e.name));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 12: 自监督层测试 (P3)
|
|
4
|
+
*
|
|
5
|
+
* runProjectTests 的探测逻辑与失败提取。全部 mock,不跑真实测试。
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
const vitest_1 = require("vitest");
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const agent_supervision_1 = require("./agent-supervision");
|
|
11
|
+
vitest_1.vi.mock("child_process", () => ({
|
|
12
|
+
execSync: vitest_1.vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
const mockExecSync = vitest_1.vi.mocked(child_process_1.execSync);
|
|
15
|
+
(0, vitest_1.beforeEach)(() => {
|
|
16
|
+
vitest_1.vi.clearAllMocks();
|
|
17
|
+
});
|
|
18
|
+
(0, vitest_1.describe)("agent-supervision", () => {
|
|
19
|
+
(0, vitest_1.it)("package.json 有 test script → npm test,失败时提取失败行", () => {
|
|
20
|
+
mockExecSync.mockImplementation(() => {
|
|
21
|
+
const err = new Error("Command failed");
|
|
22
|
+
err.stdout = "FAIL src/auth.test.ts\nAssertionError: token 无效\n 12 passing\n 1 failing\n";
|
|
23
|
+
err.stderr = "";
|
|
24
|
+
throw err;
|
|
25
|
+
});
|
|
26
|
+
// 真实 npm 项目路径下才能探测到 package.json —— 用临时脚本验证探测逻辑
|
|
27
|
+
const fs = require("fs");
|
|
28
|
+
const os = require("os");
|
|
29
|
+
const path = require("path");
|
|
30
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-test-"));
|
|
31
|
+
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ scripts: { test: "vitest run" } }));
|
|
32
|
+
const r = (0, agent_supervision_1.runProjectTests)(dir, 5000);
|
|
33
|
+
(0, vitest_1.expect)(r.ran).toBe(true);
|
|
34
|
+
(0, vitest_1.expect)(r.pass).toBe(false);
|
|
35
|
+
(0, vitest_1.expect)(r.failures.length).toBeGreaterThan(0);
|
|
36
|
+
(0, vitest_1.expect)(r.failures.join(" ")).toContain("token 无效");
|
|
37
|
+
(0, vitest_1.expect)(r.command).toBe("npm test --silent");
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.it)("测试通过时 pass=true 且 failures 为空", () => {
|
|
40
|
+
mockExecSync.mockReturnValue(" 12 passing (3s)");
|
|
41
|
+
const fs = require("fs");
|
|
42
|
+
const os = require("os");
|
|
43
|
+
const path = require("path");
|
|
44
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-test2-"));
|
|
45
|
+
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ scripts: { test: "vitest run" } }));
|
|
46
|
+
const r = (0, agent_supervision_1.runProjectTests)(dir, 5000);
|
|
47
|
+
(0, vitest_1.expect)(r.ran).toBe(true);
|
|
48
|
+
(0, vitest_1.expect)(r.pass).toBe(true);
|
|
49
|
+
(0, vitest_1.expect)(r.failures).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
(0, vitest_1.it)("无测试脚本且无 python 文件 → ran=false(调用方跳过该门)", () => {
|
|
52
|
+
const fs = require("fs");
|
|
53
|
+
const os = require("os");
|
|
54
|
+
const path = require("path");
|
|
55
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-test3-"));
|
|
56
|
+
const r = (0, agent_supervision_1.runProjectTests)(dir, 5000);
|
|
57
|
+
(0, vitest_1.expect)(r.ran).toBe(false);
|
|
58
|
+
(0, vitest_1.expect)(mockExecSync).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
});
|
package/dist/execute.js
CHANGED
|
@@ -225,6 +225,10 @@ async function execute(intent, projectPath, filePath) {
|
|
|
225
225
|
/** @requires FILE_PATH @produces COMPILE_RESULT */
|
|
226
226
|
/** @requires FILE_PATH @produces COMPILE_RESULT */
|
|
227
227
|
function verifyCompiles(filePath) {
|
|
228
|
+
// tsc 报错行以「相对 tsconfig 的文件路径」开头(如 `login_flow.ts(7,13):`),
|
|
229
|
+
// 而调用方可能传绝对路径——两种形态都要匹配,否则编译门会静默漏报。
|
|
230
|
+
const base = path.basename(filePath);
|
|
231
|
+
const isMatch = (l) => l.includes(filePath) || l.startsWith(base + "(") || l.startsWith(base + ":");
|
|
228
232
|
try {
|
|
229
233
|
const { execSync } = require("child_process");
|
|
230
234
|
const result = execSync(`npx tsc --noEmit --project tsconfig.json --pretty false 2>&1`, {
|
|
@@ -233,13 +237,13 @@ function verifyCompiles(filePath) {
|
|
|
233
237
|
stdio: "pipe",
|
|
234
238
|
});
|
|
235
239
|
// tsc exits 0, check if our file is mentioned in output anyway (unlikely but safe)
|
|
236
|
-
const lines = result.split("\n").filter(
|
|
240
|
+
const lines = result.split("\n").filter(isMatch);
|
|
237
241
|
return { pass: lines.length === 0, errors: lines };
|
|
238
242
|
}
|
|
239
243
|
catch (e) {
|
|
240
244
|
// tsc exits non-zero — parse stderr/stdout for our file's errors
|
|
241
245
|
const output = (e.stdout || "") + (e.stderr || "");
|
|
242
|
-
const lines = output.split("\n").filter(
|
|
246
|
+
const lines = output.split("\n").filter(isMatch);
|
|
243
247
|
return { pass: lines.length === 0, errors: lines };
|
|
244
248
|
}
|
|
245
249
|
}
|
package/dist/extract-ir.js
CHANGED
|
@@ -121,8 +121,10 @@ function parseProtocolFromJSDoc(node) {
|
|
|
121
121
|
const preMatch = text.match(/pre_states\s*=\s*\[([^\]]*)\]/);
|
|
122
122
|
const postMatch = text.match(/post_states\s*=\s*\[([^\]]*)\]/);
|
|
123
123
|
const invMatch = text.match(/invalidate\s*=\s*\[([^\]]*)\]/);
|
|
124
|
+
// 非规则注解(如文件头文档正文中的 "@protocol" 字样被 ts-morph 解析为 tag)
|
|
125
|
+
// → 跳过继续找下一个 @protocol tag,而不是直接放弃
|
|
124
126
|
if (!preMatch || !postMatch)
|
|
125
|
-
|
|
127
|
+
continue;
|
|
126
128
|
const namespace = nsMatch ? nsMatch[1] : undefined;
|
|
127
129
|
const pre_states = preMatch[1].split(',').map((s) => s.trim().replace(/["']/g, '')).filter(Boolean);
|
|
128
130
|
const post_states = postMatch[1].split(',').map((s) => s.trim().replace(/["']/g, '')).filter(Boolean);
|