@luckychen1993/pm-mcp 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +304 -0
  3. package/dist/acceptance-evaluator.js +330 -0
  4. package/dist/acceptance-model.js +440 -0
  5. package/dist/acceptance-report.js +335 -0
  6. package/dist/acceptance-tools.js +318 -0
  7. package/dist/audit-tools.js +61 -0
  8. package/dist/audit.js +364 -0
  9. package/dist/budget.js +71 -0
  10. package/dist/dashboard.js +241 -0
  11. package/dist/governance-audit.js +126 -0
  12. package/dist/governance-model.js +306 -0
  13. package/dist/governance-tools.js +240 -0
  14. package/dist/health.js +58 -0
  15. package/dist/index-db.js +97 -0
  16. package/dist/index-store.js +381 -0
  17. package/dist/index.js +210 -0
  18. package/dist/init.js +40 -0
  19. package/dist/knowledge-tools.js +204 -0
  20. package/dist/language-adapters.js +450 -0
  21. package/dist/language-dependencies.js +351 -0
  22. package/dist/license.js +261 -0
  23. package/dist/paths.js +51 -0
  24. package/dist/polyglot-ast.js +329 -0
  25. package/dist/portfolio.js +349 -0
  26. package/dist/project-fingerprint.js +83 -0
  27. package/dist/project-tools.js +217 -0
  28. package/dist/quality-store.js +77 -0
  29. package/dist/registry.js +38 -0
  30. package/dist/roadmap.js +96 -0
  31. package/dist/scan.js +271 -0
  32. package/dist/search.js +312 -0
  33. package/dist/security.js +269 -0
  34. package/dist/semantic-evidence-store.js +133 -0
  35. package/dist/semantic-evidence.js +100 -0
  36. package/dist/semantic-graph-algorithms.js +86 -0
  37. package/dist/semantic-graph.js +390 -0
  38. package/dist/semantic-parsers.js +311 -0
  39. package/dist/store.js +185 -0
  40. package/dist/task-tools.js +198 -0
  41. package/dist/tool-base.js +32 -0
  42. package/dist/tools.js +21 -0
  43. package/dist/types.js +208 -0
  44. package/dist/typescript-semantic.js +282 -0
  45. package/package.json +70 -0
@@ -0,0 +1,311 @@
1
+ import path from "node:path";
2
+ import { normSep } from "./budget.js";
3
+ import { analyzeTypeScriptSource } from "./typescript-semantic.js";
4
+ import { analyzePolyglotAstSource, supportsPolyglotAstLanguage } from "./polyglot-ast.js";
5
+ export const SOURCE_LANGUAGE = {
6
+ ts: ["typescript", "typescript:compiler-api-ast", 0.99], tsx: ["typescript", "typescript:compiler-api-ast", 0.99],
7
+ mts: ["typescript", "typescript:compiler-api-ast", 0.99], cts: ["typescript", "typescript:compiler-api-ast", 0.99],
8
+ js: ["javascript", "typescript:compiler-api-ast", 0.99], jsx: ["javascript", "typescript:compiler-api-ast", 0.99],
9
+ mjs: ["javascript", "typescript:compiler-api-ast", 0.99], cjs: ["javascript", "typescript:compiler-api-ast", 0.99],
10
+ py: ["python", "regex:python-import-v1", 0.82], go: ["go", "regex:go-import-v1", 0.86],
11
+ rs: ["rust", "regex:rust-use-v1", 0.76], java: ["java", "regex:jvm-import-v1", 0.86],
12
+ kt: ["kotlin", "regex:jvm-import-v1", 0.84], kts: ["kotlin", "regex:jvm-import-v1", 0.84],
13
+ cs: ["csharp", "regex:csharp-using-v1", 0.78], proto: ["protobuf", "regex:protobuf-import-v1", 0.94],
14
+ graphql: ["graphql", "regex:graphql-import-v1", 0.92], gql: ["graphql", "regex:graphql-import-v1", 0.92],
15
+ };
16
+ const RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".kt", ".kts", ".cs", ".proto", ".graphql", ".gql", ".yaml", ".yml", ".json"];
17
+ export const UNKNOWN_SOURCE_EXTS = new Set(["rb", "php", "swift", "scala", "c", "h", "cc", "cpp", "cxx", "hpp", "vue", "svelte", "lua", "sh", "bash", "zsh"]);
18
+ export function relPath(value) {
19
+ return normSep(value).replace(/^\.\//, "").replace(/^\/+/, "");
20
+ }
21
+ export function normalizedRoot(value) {
22
+ const root = relPath(value).replace(/\/$/, "");
23
+ return root === "." ? "" : root;
24
+ }
25
+ function lineLocator(content) {
26
+ const starts = [0];
27
+ for (let i = 0; i < content.length; i += 1)
28
+ if (content.charCodeAt(i) === 10)
29
+ starts.push(i + 1);
30
+ return (index) => {
31
+ let lo = 0;
32
+ let hi = starts.length;
33
+ while (lo + 1 < hi) {
34
+ const mid = (lo + hi) >>> 1;
35
+ if (starts[mid] <= index)
36
+ lo = mid;
37
+ else
38
+ hi = mid;
39
+ }
40
+ return lo + 1;
41
+ };
42
+ }
43
+ /** Replace comments and string/template literals with spaces while preserving offsets/newlines. */
44
+ function maskNonCode(content, python) {
45
+ const chars = [...content];
46
+ let quote = "";
47
+ let triple = false;
48
+ let blockComment = false;
49
+ for (let i = 0; i < chars.length; i += 1) {
50
+ const c = chars[i];
51
+ const next = chars[i + 1] ?? "";
52
+ const third = chars[i + 2] ?? "";
53
+ if (blockComment) {
54
+ if (c === "*" && next === "/") {
55
+ chars[i] = chars[i + 1] = " ";
56
+ i += 1;
57
+ blockComment = false;
58
+ }
59
+ else if (c !== "\n" && c !== "\r")
60
+ chars[i] = " ";
61
+ continue;
62
+ }
63
+ if (quote) {
64
+ if (c === "\\" && !triple) {
65
+ chars[i] = " ";
66
+ if (i + 1 < chars.length && chars[i + 1] !== "\n")
67
+ chars[++i] = " ";
68
+ continue;
69
+ }
70
+ if (triple && c === quote && next === quote && third === quote) {
71
+ chars[i] = chars[i + 1] = chars[i + 2] = " ";
72
+ i += 2;
73
+ quote = "";
74
+ triple = false;
75
+ }
76
+ else if (!triple && c === quote) {
77
+ chars[i] = " ";
78
+ quote = "";
79
+ }
80
+ else if (c !== "\n" && c !== "\r")
81
+ chars[i] = " ";
82
+ continue;
83
+ }
84
+ if (!python && c === "/" && next === "*") {
85
+ chars[i] = chars[i + 1] = " ";
86
+ i += 1;
87
+ blockComment = true;
88
+ continue;
89
+ }
90
+ if ((!python && c === "/" && next === "/") || (python && c === "#")) {
91
+ while (i < chars.length && chars[i] !== "\n")
92
+ chars[i++] = " ";
93
+ i -= 1;
94
+ continue;
95
+ }
96
+ if (c === "\"" || c === "'" || (!python && c === "`")) {
97
+ triple = python && next === c && third === c;
98
+ quote = c;
99
+ chars[i] = " ";
100
+ if (triple) {
101
+ chars[i + 1] = chars[i + 2] = " ";
102
+ i += 2;
103
+ }
104
+ }
105
+ }
106
+ return chars.join("");
107
+ }
108
+ export function referencesFor(language, parser, content) {
109
+ const out = [];
110
+ const lineAt = lineLocator(content);
111
+ const callableCode = maskNonCode(content, language === "python");
112
+ const add = (re, kind, confidence, group = 1, forceLocal = false) => {
113
+ for (const match of content.matchAll(re)) {
114
+ const specifier = match[group]?.trim();
115
+ if (specifier)
116
+ out.push({ kind, specifier, line: lineAt(match.index ?? 0), parser, confidence, forceLocal });
117
+ }
118
+ };
119
+ const addBoundCalls = (binding, specifier, namespace, callParser, confidence) => {
120
+ if (!/^[A-Za-z_$][\w$]*$/.test(binding))
121
+ return;
122
+ const escaped = binding.replace(/[$]/g, "\\$");
123
+ const re = namespace
124
+ ? new RegExp(`\\b${escaped}\\s*(?:\\?\\.|\\.)\\s*[A-Za-z_$][\\w$]*\\s*\\(`, "g")
125
+ : new RegExp(`\\b${escaped}\\s*(?:\\?\\.)?\\(`, "g");
126
+ for (const match of callableCode.matchAll(re)) {
127
+ out.push({ kind: "call", specifier, line: lineAt(match.index ?? 0), parser: callParser, confidence, offset: match.index ?? 0 });
128
+ }
129
+ };
130
+ if (language === "typescript" || language === "javascript") {
131
+ add(/\b(?:import|export)\s+(?:type\s+)?(?:[^;"']*?\s+from\s*)?["']([^"']+)["']/g, "import", 0.88);
132
+ add(/\brequire\s*\(\s*["']([^"']+)["']\s*\)/g, "import", 0.84);
133
+ add(/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g, "import", 0.82);
134
+ for (const match of content.matchAll(/\bimport\s+(?!type\b)(?:[A-Za-z_$][\w$]*\s*,\s*)?\{([^}]+)\}\s+from\s*["']([^"']+)["']/g)) {
135
+ for (const item of match[1].split(",")) {
136
+ const part = item.trim();
137
+ if (!part || part.startsWith("type "))
138
+ continue;
139
+ const names = part.split(/\s+as\s+/);
140
+ addBoundCalls((names[1] ?? names[0]).trim(), match[2], false, "regex:ts-js-import-call-v1", 0.72);
141
+ }
142
+ }
143
+ for (const match of content.matchAll(/\bimport\s+(?:[A-Za-z_$][\w$]*\s*,\s*)?\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s*["']([^"']+)["']/g))
144
+ addBoundCalls(match[1], match[2], true, "regex:ts-js-import-call-v1", 0.74);
145
+ for (const match of content.matchAll(/\bimport\s+(?!type\b)([A-Za-z_$][\w$]*)\s*(?:,\s*(?:\{[^}]*\}|\*\s+as\s+[A-Za-z_$][\w$]*))?\s+from\s*["']([^"']+)["']/g))
146
+ addBoundCalls(match[1], match[2], false, "regex:ts-js-import-call-v1", 0.72);
147
+ }
148
+ else if (language === "python") {
149
+ for (const match of content.matchAll(/^\s*from\s+([.\w]+)\s+import\s+([^#\n]+)/gm)) {
150
+ let specifier = match[1];
151
+ if (/^\.+$/.test(specifier)) {
152
+ const imported = match[2].split(",")[0].trim().split(/\s+as\s+/)[0];
153
+ if (imported && imported !== "*")
154
+ specifier += imported;
155
+ }
156
+ out.push({ kind: "import", specifier, line: lineAt(match.index ?? 0), parser, confidence: 0.84 });
157
+ for (const item of match[2].split(",")) {
158
+ const names = item.trim().replace(/[()]/g, "").split(/\s+as\s+/);
159
+ const original = names[0];
160
+ if (!original || original === "*")
161
+ continue;
162
+ const binding = (names[1] ?? original).trim();
163
+ const target = /^\.+$/.test(match[1]) ? match[1] + original : match[1];
164
+ addBoundCalls(binding, target, false, "regex:python-import-call-v1", 0.68);
165
+ addBoundCalls(binding, target, true, "regex:python-import-call-v1", 0.7);
166
+ }
167
+ }
168
+ for (const match of content.matchAll(/^\s*import\s+([^#\n]+)/gm)) {
169
+ for (const item of match[1].split(",")) {
170
+ const specifier = item.trim().split(/\s+as\s+/)[0];
171
+ if (!specifier)
172
+ continue;
173
+ out.push({ kind: "import", specifier, line: lineAt(match.index ?? 0), parser, confidence: 0.82 });
174
+ const alias = item.trim().split(/\s+as\s+/)[1] ?? specifier.split(".")[0];
175
+ addBoundCalls(alias, specifier, false, "regex:python-import-call-v1", 0.66);
176
+ addBoundCalls(alias, specifier, true, "regex:python-import-call-v1", 0.7);
177
+ }
178
+ }
179
+ }
180
+ else if (language === "go") {
181
+ add(/\bimport\s+(?:[\w.]+\s+)?["`]([^"`]+)["`]/g, "import", 0.88);
182
+ for (const block of content.matchAll(/\bimport\s*\(([\s\S]*?)\)/g))
183
+ for (const item of block[1].matchAll(/["`]([^"`]+)["`]/g))
184
+ out.push({ kind: "import", specifier: item[1], line: lineAt((block.index ?? 0) + (item.index ?? 0)), parser, confidence: 0.86 });
185
+ }
186
+ else if (language === "rust") {
187
+ add(/^\s*use\s+([^;]+);/gm, "import", 0.78);
188
+ add(/^\s*mod\s+([A-Za-z_]\w*)\s*;/gm, "import", 0.86, 1, true);
189
+ }
190
+ else if (language === "java" || language === "kotlin")
191
+ add(/^\s*import\s+(?:static\s+)?([\w.*]+)(?:\s+as\s+\w+)?\s*;?/gm, "import", 0.86);
192
+ else if (language === "csharp")
193
+ add(/^\s*(?:global\s+)?using\s+(?:\w+\s*=\s*)?([\w.]+)\s*;/gm, "import", 0.78);
194
+ else if (language === "protobuf")
195
+ add(/^\s*import\s+(?:public\s+|weak\s+)?["']([^"']+)["']\s*;/gm, "contract", 0.95, 1, true);
196
+ else if (language === "graphql")
197
+ add(/^\s*#import\s+["']([^"']+)["']/gm, "contract", 0.9, 1, true);
198
+ add(/["'`]([^"'`\n]+\.(?:proto|graphql|gql))(?:#[^"'`]*)?["'`]/gi, "contract", 0.88, 1, true);
199
+ add(/["'`]([^"'`\n]*(?:openapi|swagger|asyncapi|contracts?|schemas?)[^"'`\n]*\.(?:ya?ml|json))(?:#[^"'`]*)?["'`]/gi, "contract", 0.82, 1, true);
200
+ add(/\$ref\s*[:=]\s*["']([^"']+)["']/gi, "contract", 0.86, 1, true);
201
+ add(/\b(?:fetch|axios\.(?:get|post|put|patch|delete)|requests\.(?:get|post|put|patch|delete)|http\.(?:Get|Post)|GrpcChannel\.ForAddress)\s*\(\s*["']([^"']+)["']/g, "http", 0.68);
202
+ add(/\b(?:grpc\.(?:Dial|insecure_channel|secure_channel)|new\s+\w*(?:Client|Stub))\s*\(?\s*["']?([^"'\s,)]+)?/g, "grpc", 0.56);
203
+ add(/(?:DllImport\s*\(|ctypes\.(?:CDLL|PyDLL)\s*\(|dlopen\s*\(|ffi\.Library\s*\()\s*["']([^"']+)["']/g, "ffi", 0.82);
204
+ if (/extern\s+["']C["']|import\s+["']C["']/.test(content))
205
+ out.push({ kind: "ffi", specifier: "C ABI", line: 1, parser, confidence: 0.72 });
206
+ if (/\b(?:gql|graphql)\s*`|\b(?:useQuery|useMutation)\s*\(/.test(content))
207
+ out.push({ kind: "contract", specifier: "inline GraphQL", line: 1, parser, confidence: 0.62 });
208
+ return [...new Map(out.map((r) => [`${r.kind}|${r.line}|${r.specifier}|${r.kind === "call" ? r.offset ?? "" : ""}`, r])).values()];
209
+ }
210
+ /**
211
+ * Select the strongest built-in analyzer. Regex extraction is retained only as an
212
+ * explicit, provenance-labelled fallback for languages without an embedded parser.
213
+ */
214
+ export function analyzeSource(rel, language, parser, content) {
215
+ if (language === "typescript" || language === "javascript") {
216
+ try {
217
+ return analyzeTypeScriptSource(rel, content);
218
+ }
219
+ catch (error) {
220
+ const fallback = "regex:ts-js-fallback-v2";
221
+ return {
222
+ references: referencesFor(language, fallback, content),
223
+ exports: [],
224
+ analyzer: {
225
+ id: fallback,
226
+ family: "regex-fallback",
227
+ assurance: "heuristic",
228
+ engine: "pm-mcp masked-regex fallback",
229
+ version: "2",
230
+ capabilities: ["imports", "calls", "contracts", "http", "grpc", "ffi"],
231
+ },
232
+ status: "partial",
233
+ diagnostics: [`TypeScript compiler analyzer failed: ${error instanceof Error ? error.message : String(error)}`],
234
+ limitations: ["Compiler AST analysis failed; all emitted relations are heuristic fallback evidence."],
235
+ };
236
+ }
237
+ }
238
+ if (supportsPolyglotAstLanguage(language)) {
239
+ // Parser/ABI failures intentionally propagate: an unavailable AST provider
240
+ // must not be converted into a green heuristic result. Syntax recovery is
241
+ // represented as partial by the provider and rejected by strict policy.
242
+ return analyzePolyglotAstSource(rel, language, content);
243
+ }
244
+ const references = referencesFor(language, parser, content);
245
+ const capabilities = [...new Set(references.map((reference) => reference.kind === "export" ? "exports" : reference.kind === "import" ? "imports" : reference.kind === "call" ? "calls" : reference.kind))];
246
+ if (capabilities.length === 0)
247
+ capabilities.push("imports");
248
+ return {
249
+ references,
250
+ exports: [],
251
+ analyzer: {
252
+ id: parser,
253
+ family: "regex-fallback",
254
+ assurance: "heuristic",
255
+ engine: "pm-mcp masked-regex fallback",
256
+ version: "1",
257
+ capabilities,
258
+ },
259
+ status: "complete",
260
+ diagnostics: [],
261
+ limitations: ["No embedded language-native AST analyzer is available; emitted relations are heuristic fallback evidence."],
262
+ };
263
+ }
264
+ export function contractKind(rel, content) {
265
+ if (/\.proto$/i.test(rel) || /^\s*syntax\s*=\s*["']proto[23]["']/m.test(content))
266
+ return "protobuf";
267
+ if (/\.(graphql|gql)$/i.test(rel) || /^\s*(?:schema\s*\{|type\s+(?:Query|Mutation)\b)/m.test(content))
268
+ return "graphql";
269
+ if (/\.(?:ya?ml|json)$/i.test(rel) && /(?:^\s*openapi\s*:|["']openapi["']\s*:|^\s*swagger\s*:)/m.test(content))
270
+ return "openapi";
271
+ return null;
272
+ }
273
+ export function candidates(from, specifier, language, ownerRoots = []) {
274
+ const clean = specifier.split(/[?#]/, 1)[0].replace(/\\/g, "/");
275
+ const bases = new Set();
276
+ if (clean.startsWith(".")) {
277
+ if (language === "python" && /^\.+[\w.]*$/.test(clean)) {
278
+ const dots = clean.match(/^\.+/)?.[0].length ?? 1;
279
+ let base = path.posix.dirname(from);
280
+ for (let i = 1; i < dots; i += 1)
281
+ base = path.posix.dirname(base);
282
+ bases.add(path.posix.join(base, clean.slice(dots).replace(/\./g, "/")));
283
+ }
284
+ else
285
+ bases.add(path.posix.join(path.posix.dirname(from), clean));
286
+ }
287
+ else if (clean.startsWith("/"))
288
+ bases.add(clean.slice(1));
289
+ else {
290
+ const logical = /^(?:python|java|kotlin|csharp)$/.test(language) ? clean.replace(/\./g, "/").replace(/\/\*$/, "") : clean.replace(/::/g, "/");
291
+ bases.add(logical.replace(/^crate\//, ""));
292
+ for (const root of ownerRoots)
293
+ bases.add(path.posix.join(normalizedRoot(root), logical.replace(/^crate\//, "")));
294
+ }
295
+ const out = new Set();
296
+ for (const raw of bases) {
297
+ const base = relPath(path.posix.normalize(raw));
298
+ if (base === ".." || base.startsWith("../"))
299
+ continue;
300
+ out.add(base);
301
+ if (!path.posix.extname(base)) {
302
+ for (const ext of RESOLVE_EXTS)
303
+ out.add(base + ext);
304
+ for (const ext of RESOLVE_EXTS)
305
+ out.add(`${base}/index${ext}`);
306
+ out.add(`${base}/__init__.py`);
307
+ out.add(`${base}/mod.rs`);
308
+ }
309
+ }
310
+ return [...out];
311
+ }
package/dist/store.js ADDED
@@ -0,0 +1,185 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { DebugLogFileSchema, FeaturesFileSchema, FileNotesFileSchema, now, ProjectSchema, RoadmapFileSchema, SecurityFileSchema, SessionsFileSchema, TasksFileSchema, } from "./types.js";
4
+ import { DEBUGLOG_JSON, EXTRA_RULES_FILE, FEATURES_JSON, FILE_NOTES_JSON, PROJECT_JSON, ROADMAP_JSON, SECURITY_JSON, SESSIONS_JSON, TASKS_JSON, pmPath, } from "./paths.js";
5
+ import { ExtraRulesFileSchema } from "./types.js";
6
+ /* ------------------------------ 通用 JSON 读写 ------------------------------ */
7
+ /**
8
+ * 原子写:唯一命名临时文件 + rename。
9
+ * Windows 上目标被并发的读/写短暂持有时 rename 可能抛 EPERM/EACCES/EBUSY,
10
+ * 做指数退避重试;最终失败则清理 tmp 后抛出(不留半截文件)。
11
+ */
12
+ export function atomicWrite(file, content) {
13
+ const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
14
+ fs.writeFileSync(tmp, content, "utf8");
15
+ for (let i = 0;; i++) {
16
+ try {
17
+ fs.renameSync(tmp, file);
18
+ return;
19
+ }
20
+ catch (e) {
21
+ const code = e.code;
22
+ if ((code === "EPERM" || code === "EACCES" || code === "EBUSY") && i < 20) {
23
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5 + i * 5);
24
+ continue;
25
+ }
26
+ try {
27
+ fs.rmSync(tmp, { force: true });
28
+ }
29
+ catch {
30
+ /* 尽力清理 */
31
+ }
32
+ throw e;
33
+ }
34
+ }
35
+ }
36
+ /** 原子写 JSON(经 schema 校验后的账本数据) */
37
+ export function saveJson(file, data) {
38
+ atomicWrite(file, JSON.stringify(data, null, 2) + "\n");
39
+ }
40
+ /**
41
+ * 账本互斥锁:把「读账本 → 改 → 写」整个事务串行化,多客户端(如 ZCode+Codex 并行)
42
+ * 不再互相覆盖丢数据。锁文件 .pm/.lock(O_EXCL 创建);持有超过 10s 视为残留强制接管
43
+ * (进程崩溃不留死锁);等待上限 3s,超时明确报错而非静默丢写。
44
+ */
45
+ export function withLedgerLock(root, fn) {
46
+ const lockFile = pmPath(root, ".lock");
47
+ fs.mkdirSync(pmPath(root), { recursive: true });
48
+ const acquire = () => {
49
+ try {
50
+ fs.writeFileSync(lockFile, String(process.pid), { flag: "wx" });
51
+ return true;
52
+ }
53
+ catch {
54
+ try {
55
+ const st = fs.statSync(lockFile);
56
+ if (Date.now() - st.mtimeMs > 10_000) {
57
+ fs.rmSync(lockFile, { force: true });
58
+ try {
59
+ fs.writeFileSync(lockFile, String(process.pid), { flag: "wx" });
60
+ return true;
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
66
+ }
67
+ catch {
68
+ /* 锁文件刚好消失,下轮重试 */
69
+ }
70
+ return false;
71
+ }
72
+ };
73
+ let held = false;
74
+ for (let i = 0; i < 150 && !held; i++) {
75
+ held = acquire();
76
+ if (!held)
77
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
78
+ }
79
+ if (!held) {
80
+ throw new Error("账本锁获取超时(3s):另一进程正在写入本项目,请稍后重试。");
81
+ }
82
+ try {
83
+ return fn();
84
+ }
85
+ finally {
86
+ try {
87
+ fs.rmSync(lockFile, { force: true });
88
+ }
89
+ catch {
90
+ /* 尽力释放 */
91
+ }
92
+ }
93
+ }
94
+ export function loadJson(file, schema, fallback) {
95
+ if (!fs.existsSync(file))
96
+ return fallback;
97
+ let raw;
98
+ try {
99
+ raw = JSON.parse(fs.readFileSync(file, "utf8"));
100
+ }
101
+ catch (e) {
102
+ throw new Error(`JSON 解析失败: ${file}: ${e.message}`);
103
+ }
104
+ const parsed = schema.safeParse(raw);
105
+ if (!parsed.success) {
106
+ throw new Error(`数据校验失败: ${file}: ${parsed.error.issues
107
+ .map((i) => `${i.path.join(".")}: ${i.message}`)
108
+ .join("; ")}`);
109
+ }
110
+ return parsed.data;
111
+ }
112
+ /* ------------------------------- 各账本读写 ------------------------------- */
113
+ export function loadProject(root) {
114
+ const file = pmPath(root, PROJECT_JSON);
115
+ if (!fs.existsSync(file)) {
116
+ throw new Error(`项目未初始化(缺少 ${file}),请先调用 init_project。`);
117
+ }
118
+ // budgets 的各字段在 schema 内有默认值;对空对象 parse 即可补全
119
+ const data = loadJson(file, ProjectSchema);
120
+ if (data === undefined) {
121
+ throw new Error(`项目未初始化(缺少 ${file}),请先调用 init_project。`);
122
+ }
123
+ return data;
124
+ }
125
+ export function saveProject(root, project) {
126
+ project.updated = now();
127
+ saveJson(pmPath(root, PROJECT_JSON), ProjectSchema.parse(project));
128
+ }
129
+ export function loadTasks(root) {
130
+ return loadJson(pmPath(root, TASKS_JSON), TasksFileSchema, { seq: 0, tasks: [] });
131
+ }
132
+ export function saveTasks(root, data) {
133
+ // 写入即校验:绕过工具层直写也过不了 schema(空白标题/非法枚举在闸门被拒)
134
+ saveJson(pmPath(root, TASKS_JSON), TasksFileSchema.parse(data));
135
+ }
136
+ export function loadRoadmap(root) {
137
+ return loadJson(pmPath(root, ROADMAP_JSON), RoadmapFileSchema, { seq: 0, milestones: [] });
138
+ }
139
+ export function saveRoadmap(root, data) {
140
+ saveJson(pmPath(root, ROADMAP_JSON), RoadmapFileSchema.parse(data));
141
+ }
142
+ export function loadFeatures(root) {
143
+ return loadJson(pmPath(root, FEATURES_JSON), FeaturesFileSchema, { seq: 0, features: [] });
144
+ }
145
+ export function saveFeatures(root, data) {
146
+ saveJson(pmPath(root, FEATURES_JSON), FeaturesFileSchema.parse(data));
147
+ }
148
+ export function loadSessions(root) {
149
+ return loadJson(pmPath(root, SESSIONS_JSON), SessionsFileSchema, { seq: 0, sessions: [] });
150
+ }
151
+ export function saveSessions(root, data) {
152
+ saveJson(pmPath(root, SESSIONS_JSON), SessionsFileSchema.parse(data));
153
+ }
154
+ export function loadDebugLog(root) {
155
+ return loadJson(pmPath(root, DEBUGLOG_JSON), DebugLogFileSchema, { seq: 0, entries: [] });
156
+ }
157
+ export function saveDebugLog(root, data) {
158
+ saveJson(pmPath(root, DEBUGLOG_JSON), DebugLogFileSchema.parse(data));
159
+ }
160
+ export function loadFileNotes(root) {
161
+ return loadJson(pmPath(root, FILE_NOTES_JSON), FileNotesFileSchema, { notes: {} });
162
+ }
163
+ export function saveFileNotes(root, data) {
164
+ saveJson(pmPath(root, FILE_NOTES_JSON), FileNotesFileSchema.parse(data));
165
+ }
166
+ export function loadSecurity(root) {
167
+ return loadJson(pmPath(root, SECURITY_JSON), SecurityFileSchema, {
168
+ findings: [],
169
+ last_scan: null,
170
+ });
171
+ }
172
+ export function saveSecurity(root, data) {
173
+ saveJson(pmPath(root, SECURITY_JSON), SecurityFileSchema.parse(data));
174
+ }
175
+ export function loadExtraRules(root) {
176
+ return loadJson(pmPath(root, EXTRA_RULES_FILE), ExtraRulesFileSchema, { rules: [] });
177
+ }
178
+ /* --------------------------------- ID 生成 -------------------------------- */
179
+ export function nextId(prefix, seq, width = 3) {
180
+ return `${prefix}-${String(seq + 1).padStart(width, "0")}`;
181
+ }
182
+ /** 判断文件是否被 .pm 之外的额外忽略规则命中(v1 内置目录级忽略在 scan.ts) */
183
+ export function fileExists(root, rel) {
184
+ return fs.existsSync(path.join(root, rel));
185
+ }