progmune-runtime 3.3.8 → 3.4.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 +50 -0
- package/dist/agent-cli.js +215 -0
- package/dist/agent-loop.js +333 -0
- package/dist/agent-loop.test.js +257 -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-permissions.js +159 -0
- package/dist/agent-permissions.test.js +76 -0
- package/dist/agent-supervision.js +145 -0
- package/dist/agent-supervision.test.js +60 -0
- package/dist/audit.js +50 -3
- package/dist/check.js +62 -9
- package/dist/execute.js +6 -2
- package/dist/extract-ir.js +3 -1
- package/dist/patrol-cli.js +142 -0
- package/dist/planner-prompts.js +3 -0
- package/dist/planner.js +2 -0
- package/dist/protocol-registry.js +20 -5
- package/dist/ssg-validator.js +18 -2
- package/dist/trust/engine.js +81 -0
- package/package.json +4 -1
|
@@ -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/audit.js
CHANGED
|
@@ -66,7 +66,11 @@ function auditDirectory(dir, threshold = DEFAULT_THRESHOLD) {
|
|
|
66
66
|
if (!stat.isDirectory()) {
|
|
67
67
|
return result;
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
// Allowlist(.progmune_allowlist):豁免名单中的文件不参与覆盖率分母——
|
|
70
|
+
// 祖父条款:存量手写代码一次入册,新文件仍受覆盖要求约束。
|
|
71
|
+
// 匹配相对 allowlist 所在目录(通常为项目根),而非被扫描目录。
|
|
72
|
+
const allowlist = loadAllowlist(dir);
|
|
73
|
+
scanDir(dir, dir, result, allowlist);
|
|
70
74
|
result.coverage = result.totalFiles > 0
|
|
71
75
|
? result.progmuneFiles / result.totalFiles
|
|
72
76
|
: 0;
|
|
@@ -84,7 +88,46 @@ function auditDirectory(dir, threshold = DEFAULT_THRESHOLD) {
|
|
|
84
88
|
}
|
|
85
89
|
const MARKER_REGEX = /@progmune-generated\s+session=(\S+)(?:\s+timestamp=(\S+))?(?:\s+ruleHash=(\S+))?/;
|
|
86
90
|
const EXCLUDED_DIRS = new Set(["node_modules", ".git", "dist", ".progmune_corpus", ".progmune_memory"]);
|
|
87
|
-
|
|
91
|
+
/** 读取 .progmune_allowlist:从被扫描目录向上查找,返回匹配函数(相对 allowlist 所在目录)。 */
|
|
92
|
+
function loadAllowlist(dir) {
|
|
93
|
+
let root = dir;
|
|
94
|
+
let allowlistPath = "";
|
|
95
|
+
for (let i = 0; i < 4; i++) {
|
|
96
|
+
const p = path.join(root, ".progmune_allowlist");
|
|
97
|
+
if (fs.existsSync(p)) {
|
|
98
|
+
allowlistPath = p;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
const parent = path.dirname(root);
|
|
102
|
+
if (parent === root)
|
|
103
|
+
break;
|
|
104
|
+
root = parent;
|
|
105
|
+
}
|
|
106
|
+
if (!allowlistPath)
|
|
107
|
+
return { root: dir, match: () => false };
|
|
108
|
+
try {
|
|
109
|
+
const patterns = fs.readFileSync(allowlistPath, "utf-8")
|
|
110
|
+
.split("\n")
|
|
111
|
+
.map((l) => l.trim())
|
|
112
|
+
.filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
113
|
+
return {
|
|
114
|
+
root: path.dirname(allowlistPath),
|
|
115
|
+
match: (relPath) => {
|
|
116
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
117
|
+
return patterns.some((p) => {
|
|
118
|
+
const pat = p.replace(/^\.\//, ""); // 兼容 "./foo.ts" 写法
|
|
119
|
+
if (pat.endsWith("*"))
|
|
120
|
+
return normalized.startsWith(pat.slice(0, -1));
|
|
121
|
+
return normalized === pat || normalized.startsWith(pat.replace(/\/$/, "") + "/");
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return { root: dir, match: () => false };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function scanDir(rootDir, currentDir, result, allowlist) {
|
|
88
131
|
let entries;
|
|
89
132
|
try {
|
|
90
133
|
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
@@ -96,7 +139,7 @@ function scanDir(rootDir, currentDir, result) {
|
|
|
96
139
|
const fullPath = path.join(currentDir, entry.name);
|
|
97
140
|
if (entry.isDirectory()) {
|
|
98
141
|
if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
99
|
-
scanDir(rootDir, fullPath, result);
|
|
142
|
+
scanDir(rootDir, fullPath, result, allowlist);
|
|
100
143
|
}
|
|
101
144
|
continue;
|
|
102
145
|
}
|
|
@@ -104,6 +147,10 @@ function scanDir(rootDir, currentDir, result) {
|
|
|
104
147
|
if (!entry.name.endsWith(".ts") && !entry.name.endsWith(".tsx") && !entry.name.endsWith(".mjs")) {
|
|
105
148
|
continue;
|
|
106
149
|
}
|
|
150
|
+
// 豁免名单(祖父条款):不在覆盖率分母内——相对 allowlist 根匹配
|
|
151
|
+
const rel = path.relative(allowlist.root, fullPath);
|
|
152
|
+
if (allowlist.match(rel))
|
|
153
|
+
continue;
|
|
107
154
|
result.totalFiles++;
|
|
108
155
|
try {
|
|
109
156
|
const content = fs.readFileSync(fullPath, "utf-8");
|
package/dist/check.js
CHANGED
|
@@ -117,10 +117,16 @@ if (cliArg === "--ledger") {
|
|
|
117
117
|
const rebuilt = (0, ssg_validator_1.rebuildState)(allTransitions, nsInit);
|
|
118
118
|
const lastTransition = allTransitions[allTransitions.length - 1];
|
|
119
119
|
const recorded = lastTransition.statesAfter;
|
|
120
|
-
|
|
120
|
+
// 与 checkLedgerConsistency 同规则:只比较 recorded 中有过非空快照的 ns——
|
|
121
|
+
// 早期 session 对 file/db 等记录空数组(无信息),不参与比较。
|
|
122
|
+
const informativeNs = new Set();
|
|
123
|
+
for (const [ns, s] of Object.entries(recorded)) {
|
|
124
|
+
if ((s || []).length > 0)
|
|
125
|
+
informativeNs.add(ns);
|
|
126
|
+
}
|
|
121
127
|
const norm = (snap) => {
|
|
122
128
|
const out = {};
|
|
123
|
-
for (const ns of [...
|
|
129
|
+
for (const ns of [...informativeNs].sort())
|
|
124
130
|
out[ns] = [...(snap[ns] || [])].sort();
|
|
125
131
|
return out;
|
|
126
132
|
};
|
|
@@ -307,6 +313,35 @@ else {
|
|
|
307
313
|
}
|
|
308
314
|
}
|
|
309
315
|
// ── 4. Ledger 不变量检查 (Phase 3) ──
|
|
316
|
+
/**
|
|
317
|
+
* 历史约定兼容:早期 session 以 "INIT" 作为 _global 初始状态
|
|
318
|
+
* (当前约定 UNAUTHENTICATED;当前 148 条协议规则已不含 INIT 状态)。
|
|
319
|
+
* 返回规范化视图(仅比较用,不改盘)与受影响转移数。
|
|
320
|
+
*/
|
|
321
|
+
function normalizeLegacyInit(transitions, nsInit) {
|
|
322
|
+
let migrated = 0;
|
|
323
|
+
const view = transitions.map((t) => {
|
|
324
|
+
const normSnap = (snap) => {
|
|
325
|
+
const out = {};
|
|
326
|
+
let changed = false;
|
|
327
|
+
for (const [ns, states] of Object.entries(snap)) {
|
|
328
|
+
const currentInit = nsInit.get(ns);
|
|
329
|
+
if (currentInit && currentInit !== "INIT" && states.length === 1 && states[0] === "INIT") {
|
|
330
|
+
out[ns] = [currentInit];
|
|
331
|
+
changed = true;
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
out[ns] = [...states];
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (changed)
|
|
338
|
+
migrated++;
|
|
339
|
+
return out;
|
|
340
|
+
};
|
|
341
|
+
return { ...t, statesBefore: normSnap(t.statesBefore), statesAfter: normSnap(t.statesAfter) };
|
|
342
|
+
});
|
|
343
|
+
return { view, migrated };
|
|
344
|
+
}
|
|
310
345
|
step("4/6 Ledger 不变量");
|
|
311
346
|
{
|
|
312
347
|
// Load namespace initial states from protocols.json for correct replay
|
|
@@ -314,6 +349,7 @@ step("4/6 Ledger 不变量");
|
|
|
314
349
|
let checked = 0;
|
|
315
350
|
let consistent = 0;
|
|
316
351
|
let stateMatch = 0;
|
|
352
|
+
let legacyInitMigrations = 0;
|
|
317
353
|
const allLedgers = [];
|
|
318
354
|
const violationsDetail = [];
|
|
319
355
|
const replayMismatchDetail = [];
|
|
@@ -335,9 +371,15 @@ step("4/6 Ledger 不变量");
|
|
|
335
371
|
const transitions = attempt.transitions || [];
|
|
336
372
|
if (transitions.length === 0)
|
|
337
373
|
continue;
|
|
338
|
-
|
|
374
|
+
// 历史约定兼容:早期 session 以 "INIT" 作为 _global 初始状态
|
|
375
|
+
// (当前约定 UNAUTHENTICATED;当前协议规则已不含 INIT)。
|
|
376
|
+
// 规范化视图只用于比较,不改盘。
|
|
377
|
+
const { view, migrated } = normalizeLegacyInit(transitions, nsInit);
|
|
378
|
+
if (migrated > 0)
|
|
379
|
+
legacyInitMigrations++;
|
|
380
|
+
allLedgers.push(...view);
|
|
339
381
|
// Invariant check
|
|
340
|
-
const result = (0, ssg_validator_1.checkLedgerConsistency)(
|
|
382
|
+
const result = (0, ssg_validator_1.checkLedgerConsistency)(view, nsInit);
|
|
341
383
|
checked++;
|
|
342
384
|
if (result.consistent) {
|
|
343
385
|
consistent++;
|
|
@@ -348,10 +390,16 @@ step("4/6 Ledger 不变量");
|
|
|
348
390
|
}
|
|
349
391
|
}
|
|
350
392
|
// Replay check: rebuildState === recorded statesAfter
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
393
|
+
// 只比较 recorded 中有过非空快照的 ns(早期 session 对部分 ns
|
|
394
|
+
// 记录空数组,不携带可比较的状态信息)
|
|
395
|
+
const rebuilt = (0, ssg_validator_1.rebuildState)(view, nsInit);
|
|
396
|
+
const recorded = view[view.length - 1].statesAfter;
|
|
397
|
+
const informativeNs = new Set();
|
|
398
|
+
for (const [ns, s] of Object.entries(recorded)) {
|
|
399
|
+
if ((s || []).length > 0)
|
|
400
|
+
informativeNs.add(ns);
|
|
401
|
+
}
|
|
402
|
+
if (JSON.stringify(norm(rebuilt, informativeNs)) === JSON.stringify(norm(recorded, informativeNs))) {
|
|
355
403
|
stateMatch++;
|
|
356
404
|
}
|
|
357
405
|
else {
|
|
@@ -368,6 +416,9 @@ step("4/6 Ledger 不变量");
|
|
|
368
416
|
else if (consistent === checked && stateMatch === checked) {
|
|
369
417
|
const combinedHash = (0, ssg_validator_1.hashLedger)(allLedgers);
|
|
370
418
|
pass(`全部 ${checked} 个 Ledger 通过 (Invariant-0 + Invariant-1 + Replay) | 完整性指纹: ${combinedHash}`);
|
|
419
|
+
if (legacyInitMigrations > 0) {
|
|
420
|
+
warn(`历史约定兼容:${legacyInitMigrations} 条转移的初始状态 INIT 已按当前约定(${nsInit.get("_global")})规范化比较(不改盘)`);
|
|
421
|
+
}
|
|
371
422
|
}
|
|
372
423
|
else {
|
|
373
424
|
if (consistent < checked) {
|
|
@@ -404,7 +455,9 @@ step("4/6 Ledger 不变量");
|
|
|
404
455
|
if (transitions.length === 0)
|
|
405
456
|
continue;
|
|
406
457
|
try {
|
|
407
|
-
|
|
458
|
+
// 历史约定兼容:与主检查一致,先规范化旧 INIT 初值
|
|
459
|
+
const { view } = normalizeLegacyInit(transitions, nsInit);
|
|
460
|
+
(0, runtime_invariants_1.assertLedgerInvariants)(view, nsInit);
|
|
408
461
|
}
|
|
409
462
|
catch (e) {
|
|
410
463
|
if (e instanceof runtime_invariants_1.InvariantViolationError) {
|
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);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 12: Progmune 免疫巡逻 CLI — `progmune patrol`(形态 B 第一版)
|
|
4
|
+
*
|
|
5
|
+
* 扫描项目 → trust_check → 违规报告 + 建议补丁(绝不自动合并)。
|
|
6
|
+
* 支持 --watch 持续监听(文件变更 → 防抖 → 重新巡逻 → 刷新报告)。
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx ts-node src/patrol-cli.ts --project <dir> [options]
|
|
10
|
+
* npm run patrol -- --project <dir> [options]
|
|
11
|
+
*
|
|
12
|
+
* Options:
|
|
13
|
+
* --project <dir> 目标项目目录(默认 CWD)
|
|
14
|
+
* --watch 持续监听模式(文件变更后自动重扫)
|
|
15
|
+
* --json JSON 输出(单次扫描)
|
|
16
|
+
* --help, -h
|
|
17
|
+
*/
|
|
18
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
21
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
22
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(o, k2, desc);
|
|
25
|
+
}) : (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
o[k2] = m[k];
|
|
28
|
+
}));
|
|
29
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
30
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
31
|
+
}) : function(o, v) {
|
|
32
|
+
o["default"] = v;
|
|
33
|
+
});
|
|
34
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
35
|
+
var ownKeys = function(o) {
|
|
36
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
37
|
+
var ar = [];
|
|
38
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
39
|
+
return ar;
|
|
40
|
+
};
|
|
41
|
+
return ownKeys(o);
|
|
42
|
+
};
|
|
43
|
+
return function (mod) {
|
|
44
|
+
if (mod && mod.__esModule) return mod;
|
|
45
|
+
var result = {};
|
|
46
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
47
|
+
__setModuleDefault(result, mod);
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
const agent_patrol_1 = require("./agent-patrol");
|
|
54
|
+
const agent_perception_1 = require("./agent-perception");
|
|
55
|
+
const agent_permissions_1 = require("./agent-permissions");
|
|
56
|
+
// 在 chdir 到项目目录之前按启动 CWD 加载 .env——
|
|
57
|
+
// trust 引擎内部的 lazy require(语义映射 LLM 回退)发生在 chdir 之后,
|
|
58
|
+
// 若不预载,LLM_API_KEY 不可用,映射降级会导致漏报。
|
|
59
|
+
try {
|
|
60
|
+
require("dotenv/config");
|
|
61
|
+
}
|
|
62
|
+
catch { /* dotenv 可选 */ }
|
|
63
|
+
const args = process.argv.slice(2);
|
|
64
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
65
|
+
console.log(`
|
|
66
|
+
Progmune 免疫巡逻 (P4) — 监听/扫描 → trust_check → 报告 + 建议补丁(永不自动合并)
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
npx ts-node src/patrol-cli.ts --project <dir> [options]
|
|
70
|
+
npm run patrol -- --project <dir> [options]
|
|
71
|
+
|
|
72
|
+
Options:
|
|
73
|
+
--project <dir> 目标项目目录(默认当前目录)
|
|
74
|
+
--watch 持续监听模式(源文件变更后自动重扫并刷新报告)
|
|
75
|
+
--json JSON 输出(单次扫描)
|
|
76
|
+
--help, -h 显示帮助
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
npm run patrol -- --project demo-patrol
|
|
80
|
+
npm run patrol -- --project demo-patrol --watch
|
|
81
|
+
`);
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
const getFlag = (name) => {
|
|
85
|
+
const idx = args.indexOf(`--${name}`);
|
|
86
|
+
return idx >= 0 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
87
|
+
};
|
|
88
|
+
const projectPath = path.resolve(getFlag("project") || process.cwd());
|
|
89
|
+
const watch = args.includes("--watch");
|
|
90
|
+
const json = args.includes("--json");
|
|
91
|
+
async function scanOnce(label) {
|
|
92
|
+
try {
|
|
93
|
+
const report = await (0, agent_patrol_1.runPatrol)(projectPath);
|
|
94
|
+
// ── P5 安全层:报告写入经 FsSandbox(巡逻预设:写=沙箱白名单) ──
|
|
95
|
+
const writeDecision = (0, agent_permissions_1.decidePermission)("patrol", {
|
|
96
|
+
level: "write",
|
|
97
|
+
target: path.join(projectPath, ".progmune_patrol_report.md"),
|
|
98
|
+
projectPath,
|
|
99
|
+
});
|
|
100
|
+
let reportFile = "";
|
|
101
|
+
if (writeDecision.allowed) {
|
|
102
|
+
reportFile = (0, agent_patrol_1.writePatrolReport)(report, projectPath);
|
|
103
|
+
}
|
|
104
|
+
if (json && !watch) {
|
|
105
|
+
console.log(JSON.stringify(report, null, 2));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.log(`[${label}] ` + (0, agent_patrol_1.formatPatrolTerminal)(report).replace(/\n/g, "\n "));
|
|
109
|
+
console.log(writeDecision.allowed
|
|
110
|
+
? ` 报告: ${reportFile}`
|
|
111
|
+
: ` ⚠️ 报告写入被沙箱拒绝: ${writeDecision.detail}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
console.error(`❌ 巡逻失败: ${e?.message || e}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function main() {
|
|
119
|
+
process.chdir(projectPath);
|
|
120
|
+
console.log(`🛡️ Progmune 免疫巡逻 (P4) — 项目: ${projectPath}${watch ? "(持续监听)" : ""}`);
|
|
121
|
+
if (!watch) {
|
|
122
|
+
await scanOnce("扫描");
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
// 持续监听:RepoWatcher 防抖触发重扫
|
|
126
|
+
await scanOnce("首次");
|
|
127
|
+
let scanning = false;
|
|
128
|
+
const watcher = new agent_perception_1.RepoWatcher(projectPath, async (file) => {
|
|
129
|
+
if (scanning)
|
|
130
|
+
return; // 扫描期间的新变更合并进下一轮
|
|
131
|
+
scanning = true;
|
|
132
|
+
console.log(`🔍 检测到变更: ${file}`);
|
|
133
|
+
await scanOnce("重扫");
|
|
134
|
+
scanning = false;
|
|
135
|
+
}, 1500);
|
|
136
|
+
watcher.start();
|
|
137
|
+
console.log("👂 监听中(Ctrl+C 退出)…");
|
|
138
|
+
}
|
|
139
|
+
main().catch((e) => {
|
|
140
|
+
console.error(`❌ 巡逻 CLI 异常: ${e?.message || e}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
});
|
package/dist/planner-prompts.js
CHANGED
|
@@ -40,6 +40,9 @@ exports.RETRY_HINT = `输出格式:紧凑 JSON 数组 [{"f":"函数名","to":"
|
|
|
40
40
|
// ── Formatters ──
|
|
41
41
|
/** Build a compact function list with parameter examples for LLM precision. */
|
|
42
42
|
function buildCompactFuncList(funcs, allFuncs) {
|
|
43
|
+
// 语义 marker(__progmune_*,提取器注入供规则消费)不是真实可调用函数——
|
|
44
|
+
// 不出现在 LLM 可见函数列表中,防止被生成成真实调用导致编译失败
|
|
45
|
+
funcs = funcs.filter((f) => !String(f.name || "").startsWith("__progmune_"));
|
|
43
46
|
// Example values for each type — helps LLM fill meaningful args
|
|
44
47
|
function exampleValue(type, paramName) {
|
|
45
48
|
const t = (type || "any").replace(/\[\]$/, "").toLowerCase();
|
package/dist/planner.js
CHANGED
|
@@ -900,6 +900,8 @@ ${planner_prompts_1.RETRY_HINT}
|
|
|
900
900
|
: await (0, llm_1.generate)(`你是程序合成助手。\n\n${currentPrompt}`);
|
|
901
901
|
}
|
|
902
902
|
catch (e) {
|
|
903
|
+
// 铁律:失败原因必须可见(不许静默绕过)——LLM 异常记录后继续重试/降级
|
|
904
|
+
console.error(`⚠️ LLM 调用失败 (attempt ${r + 1}/${maxRetries}): ${e?.message || e}`);
|
|
903
905
|
continue;
|
|
904
906
|
}
|
|
905
907
|
if (!text)
|
|
@@ -68,9 +68,22 @@ function getProtocolConfig() {
|
|
|
68
68
|
let rules = [];
|
|
69
69
|
let version = "1.0";
|
|
70
70
|
const protoPath = path.resolve(process.env.PROGMUNE_PROJECT_DIR || process.cwd(), "protocols.json");
|
|
71
|
-
|
|
71
|
+
// 解析顺序与 loadIR 一致:显式目录 → CWD → 包目录回退。
|
|
72
|
+
// 修复:在无 protocols.json 的项目目录里运行时(如 agent CLI chdir 到
|
|
73
|
+
// demo-project),此前 nsInit 退化为仅 _global —— session 只记录 1 个
|
|
74
|
+
// 命名空间,而 check 在仓库根跑用全量 27 个 ns 重建 → before-consistency
|
|
75
|
+
// 全量误报(1308/1308)。包目录回退保证任何 cwd 下世界一致。
|
|
76
|
+
const candidates = [protoPath];
|
|
77
|
+
try {
|
|
78
|
+
candidates.push(path.resolve(__dirname, "../protocols.json"));
|
|
79
|
+
}
|
|
80
|
+
catch { /* __dirname 不可用时跳过 */ }
|
|
81
|
+
let loaded = false;
|
|
82
|
+
for (const p of candidates) {
|
|
83
|
+
if (!fs.existsSync(p))
|
|
84
|
+
continue;
|
|
72
85
|
try {
|
|
73
|
-
const proto = JSON.parse(fs.readFileSync(
|
|
86
|
+
const proto = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
74
87
|
version = proto.$schema || proto.version || "1.0";
|
|
75
88
|
// Load namespace initial states
|
|
76
89
|
nsInit.set("_global", "UNAUTHENTICATED");
|
|
@@ -81,11 +94,13 @@ function getProtocolConfig() {
|
|
|
81
94
|
}
|
|
82
95
|
// Parse rules
|
|
83
96
|
rules = (0, ssg_validator_1.parseProtocolsFromJSON)(proto);
|
|
97
|
+
loaded = true;
|
|
98
|
+
break;
|
|
84
99
|
}
|
|
85
|
-
catch { /*
|
|
100
|
+
catch { /* 下一个候选 */ }
|
|
86
101
|
}
|
|
87
|
-
|
|
88
|
-
// Fallback: minimal defaults (no protocols.json
|
|
102
|
+
if (!loaded) {
|
|
103
|
+
// Fallback: minimal defaults (no protocols.json anywhere)
|
|
89
104
|
nsInit.set("_global", "UNAUTHENTICATED");
|
|
90
105
|
}
|
|
91
106
|
// Compute rule hash
|