@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,351 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ function result() {
4
+ return { dependencies: [], dependencyErrors: [] };
5
+ }
6
+ function ref(manifest, parser, confidence, name, version, scope) {
7
+ return { name: name.trim(), version: version.trim() || "*", scope, sourceManifest: manifest.path, parser, confidence };
8
+ }
9
+ function error(manifest, parser, message, line) {
10
+ return { sourceManifest: manifest.path, parser, message, ...(line === undefined ? {} : { line }) };
11
+ }
12
+ function readJson(file) {
13
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
14
+ if (!value || typeof value !== "object" || Array.isArray(value))
15
+ throw new Error("JSON root is not an object");
16
+ return value;
17
+ }
18
+ function nodeDependencies(manifest) {
19
+ const parser = "package-json", out = result();
20
+ let pkg;
21
+ try {
22
+ pkg = readJson(manifest.path);
23
+ }
24
+ catch (cause) {
25
+ out.dependencyErrors.push(error(manifest, parser, `invalid package.json: ${cause instanceof Error ? cause.message : String(cause)}`));
26
+ return out;
27
+ }
28
+ const sections = [
29
+ ["dependencies", "runtime"], ["devDependencies", "development"],
30
+ ["optionalDependencies", "optional"], ["peerDependencies", "peer"],
31
+ ];
32
+ for (const [section, scope] of sections) {
33
+ const value = pkg[section];
34
+ if (value === undefined)
35
+ continue;
36
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
37
+ out.dependencyErrors.push(error(manifest, parser, `${section} must be an object`));
38
+ continue;
39
+ }
40
+ for (const [name, version] of Object.entries(value)) {
41
+ if (!name.trim() || typeof version !== "string" || !version.trim()) {
42
+ out.dependencyErrors.push(error(manifest, parser, `cannot parse ${section} entry ${name || "<empty>"}`));
43
+ }
44
+ else
45
+ out.dependencies.push(ref(manifest, parser, 1, name, version, scope));
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+ function pep508(value) {
51
+ const base = value.split(/\s*;\s*/, 1)[0].trim();
52
+ const direct = base.match(/^([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[[^\]]+\])?\s*@\s*(\S.+)$/);
53
+ if (direct)
54
+ return { name: direct[1], version: `@ ${direct[2].trim()}` };
55
+ const match = base.match(/^([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[[^\]]+\])?\s*((?:(?:===|==|~=|!=|<=|>=|<|>)\s*[^,\s]+(?:\s*,\s*)?)*)$/);
56
+ return match ? { name: match[1], version: match[2].replace(/\s+/g, "") || "*" } : null;
57
+ }
58
+ function requirementsDependencies(manifest) {
59
+ const parser = "requirements-regex", out = result();
60
+ const scope = /(?:dev|test)/i.test(path.basename(manifest.path)) ? "development" : "runtime";
61
+ fs.readFileSync(manifest.path, "utf8").split(/\r?\n/).forEach((raw, index) => {
62
+ const line = raw.replace(/\s+#.*$/, "").trim();
63
+ if (!line || line.startsWith("#") || line.startsWith("-"))
64
+ return;
65
+ const parsed = pep508(line);
66
+ if (parsed)
67
+ out.dependencies.push(ref(manifest, parser, 0.9, parsed.name, parsed.version, scope));
68
+ else
69
+ out.dependencyErrors.push(error(manifest, parser, "unparsed requirement entry", index + 1));
70
+ });
71
+ return out;
72
+ }
73
+ function tomlSections(text) {
74
+ const sections = [];
75
+ let current = { name: "", body: "", startLine: 1 };
76
+ for (const [index, line] of text.split(/\r?\n/).entries()) {
77
+ const header = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
78
+ if (header) {
79
+ sections.push(current);
80
+ current = { name: header[1].trim(), body: "", startLine: index + 2 };
81
+ }
82
+ else
83
+ current.body += line + "\n";
84
+ }
85
+ sections.push(current);
86
+ return sections;
87
+ }
88
+ function stringArray(body, key) {
89
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
90
+ const start = new RegExp(`^\\s*${escapedKey}\\s*=\\s*\\[`, "m").exec(body);
91
+ if (!start)
92
+ return null;
93
+ const open = start.index + start[0].lastIndexOf("[");
94
+ let quote = "", escaped = false, close = -1;
95
+ for (let i = open + 1; i < body.length; i++) {
96
+ const char = body[i];
97
+ if (escaped) {
98
+ escaped = false;
99
+ continue;
100
+ }
101
+ if (quote === '"' && char === "\\") {
102
+ escaped = true;
103
+ continue;
104
+ }
105
+ if (char === '"' || char === "'") {
106
+ if (!quote)
107
+ quote = char;
108
+ else if (quote === char)
109
+ quote = "";
110
+ continue;
111
+ }
112
+ if (!quote && char === "]") {
113
+ close = i;
114
+ break;
115
+ }
116
+ }
117
+ if (close < 0)
118
+ return { values: [], malformed: true };
119
+ const inner = body.slice(open + 1, close), values = [];
120
+ const token = /"((?:\\.|[^"\\])*)"|'([^']*)'/g;
121
+ let match;
122
+ while ((match = token.exec(inner))) {
123
+ if (match[1] !== undefined) {
124
+ try {
125
+ values.push(JSON.parse(`"${match[1]}"`));
126
+ }
127
+ catch {
128
+ return { values, malformed: true };
129
+ }
130
+ }
131
+ else
132
+ values.push(match[2]);
133
+ }
134
+ const remainder = inner.replace(token, "").replace(/#.*$/gm, "").replace(/[\s,]/g, "");
135
+ return { values, malformed: Boolean(remainder) };
136
+ }
137
+ function pyprojectDependencies(manifest) {
138
+ const parser = "pyproject-toml-regex", out = result();
139
+ for (const section of tomlSections(fs.readFileSync(manifest.path, "utf8"))) {
140
+ if (section.name === "project") {
141
+ const array = stringArray(section.body, "dependencies");
142
+ if (!array)
143
+ continue;
144
+ if (array.malformed)
145
+ out.dependencyErrors.push(error(manifest, parser, "malformed project.dependencies array", section.startLine));
146
+ for (const item of array.values) {
147
+ const parsed = pep508(item);
148
+ if (parsed)
149
+ out.dependencies.push(ref(manifest, parser, 0.9, parsed.name, parsed.version, "runtime"));
150
+ else
151
+ out.dependencyErrors.push(error(manifest, parser, "unparsed project dependency", section.startLine));
152
+ }
153
+ }
154
+ else if (section.name === "project.optional-dependencies") {
155
+ const keys = [...section.body.matchAll(/^\s*([A-Za-z0-9_.-]+)\s*=\s*\[/gm)].map((match) => match[1]);
156
+ for (const key of keys) {
157
+ const array = stringArray(section.body, key);
158
+ if (array.malformed)
159
+ out.dependencyErrors.push(error(manifest, parser, `malformed optional dependency group ${key}`, section.startLine));
160
+ for (const item of array.values) {
161
+ const parsed = pep508(item);
162
+ if (parsed)
163
+ out.dependencies.push(ref(manifest, parser, 0.85, parsed.name, parsed.version, `optional:${key}`));
164
+ else
165
+ out.dependencyErrors.push(error(manifest, parser, `unparsed optional dependency in ${key}`, section.startLine));
166
+ }
167
+ }
168
+ }
169
+ else if (section.name === "tool.poetry.dependencies" || /^tool\.poetry\.group\.[^.]+\.dependencies$/.test(section.name)) {
170
+ const scope = section.name === "tool.poetry.dependencies" ? "runtime" : "development";
171
+ section.body.split(/\r?\n/).forEach((raw, offset) => {
172
+ const line = raw.replace(/\s+#.*$/, "").trim();
173
+ if (!line)
174
+ return;
175
+ const entry = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
176
+ if (!entry) {
177
+ out.dependencyErrors.push(error(manifest, parser, "unparsed poetry dependency entry", section.startLine + offset));
178
+ return;
179
+ }
180
+ if (entry[1].toLowerCase() === "python")
181
+ return;
182
+ const scalar = entry[2].match(/^["']([^"']+)["']$/)?.[1];
183
+ const inline = entry[2].match(/\bversion\s*=\s*["']([^"']+)["']/)?.[1];
184
+ const name = entry[2].match(/\bpackage\s*=\s*["']([^"']+)["']/)?.[1] ?? entry[1];
185
+ if (scalar ?? inline)
186
+ out.dependencies.push(ref(manifest, parser, 0.8, name, scalar ?? inline, scope));
187
+ else
188
+ out.dependencyErrors.push(error(manifest, parser, `unparsed poetry dependency ${entry[1]}`, section.startLine + offset));
189
+ });
190
+ }
191
+ }
192
+ return out;
193
+ }
194
+ function goDependencies(manifest) {
195
+ const parser = "go-mod-regex", out = result();
196
+ let block = false;
197
+ fs.readFileSync(manifest.path, "utf8").split(/\r?\n/).forEach((raw, index) => {
198
+ const trimmed = raw.trim();
199
+ if (/^require\s*\($/.test(trimmed)) {
200
+ block = true;
201
+ return;
202
+ }
203
+ if (block && trimmed === ")") {
204
+ block = false;
205
+ return;
206
+ }
207
+ if (!trimmed || trimmed.startsWith("//"))
208
+ return;
209
+ const candidate = block ? trimmed : trimmed.startsWith("require ") ? trimmed.slice(8).trim() : "";
210
+ if (!candidate)
211
+ return;
212
+ const indirect = /\/\/\s*indirect\s*$/.test(candidate);
213
+ const match = candidate.replace(/\s*\/\/.*$/, "").trim().match(/^(\S+)\s+(\S+)$/);
214
+ if (match)
215
+ out.dependencies.push(ref(manifest, parser, 0.95, match[1], match[2], indirect ? "indirect" : "runtime"));
216
+ else
217
+ out.dependencyErrors.push(error(manifest, parser, "unparsed require entry", index + 1));
218
+ });
219
+ if (block)
220
+ out.dependencyErrors.push(error(manifest, parser, "unterminated require block"));
221
+ return out;
222
+ }
223
+ function cargoDependencies(manifest) {
224
+ const parser = "cargo-toml-regex", out = result();
225
+ for (const section of tomlSections(fs.readFileSync(manifest.path, "utf8"))) {
226
+ const suffix = section.name.match(/(?:^|\.)(dev-dependencies|build-dependencies|dependencies)$/)?.[1];
227
+ if (!suffix)
228
+ continue;
229
+ const scope = suffix === "dev-dependencies" ? "development" : suffix === "build-dependencies" ? "build" : "runtime";
230
+ section.body.split(/\r?\n/).forEach((raw, offset) => {
231
+ const line = raw.replace(/\s+#.*$/, "").trim();
232
+ if (!line)
233
+ return;
234
+ const entry = line.match(/^["']?([^"'=\s]+)["']?\s*=\s*(.+)$/);
235
+ if (!entry) {
236
+ out.dependencyErrors.push(error(manifest, parser, "unparsed dependency entry", section.startLine + offset));
237
+ return;
238
+ }
239
+ const scalar = entry[2].match(/^["']([^"']+)["']$/)?.[1];
240
+ const version = scalar ?? entry[2].match(/\bversion\s*=\s*["']([^"']+)["']/)?.[1]
241
+ ?? (entry[2].match(/\bworkspace\s*=\s*true\b/) ? "workspace" : undefined)
242
+ ?? entry[2].match(/\bgit\s*=\s*["']([^"']+)["']/)?.[1]
243
+ ?? entry[2].match(/\bpath\s*=\s*["']([^"']+)["']/)?.[1];
244
+ const name = entry[2].match(/\bpackage\s*=\s*["']([^"']+)["']/)?.[1] ?? entry[1];
245
+ if (version)
246
+ out.dependencies.push(ref(manifest, parser, 0.9, name, version, scope));
247
+ else
248
+ out.dependencyErrors.push(error(manifest, parser, `unparsed dependency ${entry[1]}`, section.startLine + offset));
249
+ });
250
+ }
251
+ return out;
252
+ }
253
+ function xmlValue(block, tag) {
254
+ return block.match(new RegExp(`<${tag}\\b[^>]*>\\s*([^<]+?)\\s*</${tag}>`, "i"))?.[1].trim();
255
+ }
256
+ function mavenDependencies(manifest) {
257
+ const parser = "maven-xml-regex", out = result(), text = fs.readFileSync(manifest.path, "utf8");
258
+ const blocks = [...text.matchAll(/<dependency\b[^>]*>([\s\S]*?)<\/dependency>/gi)];
259
+ for (const match of blocks) {
260
+ const group = xmlValue(match[1], "groupId"), artifact = xmlValue(match[1], "artifactId");
261
+ if (!group || !artifact)
262
+ out.dependencyErrors.push(error(manifest, parser, "dependency missing groupId or artifactId"));
263
+ else
264
+ out.dependencies.push(ref(manifest, parser, 0.8, `${group}:${artifact}`, xmlValue(match[1], "version") ?? "managed", xmlValue(match[1], "scope") ?? "compile"));
265
+ }
266
+ const starts = [...text.matchAll(/<dependency\b/gi)].length;
267
+ for (let i = blocks.length; i < starts; i++)
268
+ out.dependencyErrors.push(error(manifest, parser, "unclosed or self-closing dependency element"));
269
+ return out;
270
+ }
271
+ function gradleDependencies(manifest) {
272
+ const parser = "gradle-coordinate-regex", out = result();
273
+ let depth = 0;
274
+ fs.readFileSync(manifest.path, "utf8").split(/\r?\n/).forEach((raw, index) => {
275
+ const line = raw.replace(/\/\/.*$/, "").trim(), opening = /\bdependencies\s*\{/.test(line);
276
+ if (opening)
277
+ depth += 1;
278
+ else if (depth > 0 && line && !/^[{}]+$/.test(line)) {
279
+ const entry = line.match(/^([A-Za-z][A-Za-z0-9_]*)\s*(?:\(\s*)?["']([^"']+)["']/);
280
+ if (entry) {
281
+ const parts = entry[2].split(":");
282
+ if (parts.length >= 3 && parts[0] && parts[1] && parts[2])
283
+ out.dependencies.push(ref(manifest, parser, 0.75, `${parts[0]}:${parts[1]}`, parts[2], entry[1]));
284
+ else
285
+ out.dependencyErrors.push(error(manifest, parser, `unparsed ${entry[1]} coordinate`, index + 1));
286
+ }
287
+ else if (/^[A-Za-z][A-Za-z0-9_]*\s*\(/.test(line))
288
+ out.dependencyErrors.push(error(manifest, parser, "unparsed dependency declaration", index + 1));
289
+ }
290
+ if (depth > 0)
291
+ depth += (line.match(/\{/g)?.length ?? 0) - (line.match(/\}/g)?.length ?? 0) - (opening ? 1 : 0);
292
+ if (depth < 0)
293
+ depth = 0;
294
+ });
295
+ return out;
296
+ }
297
+ function xmlAttribute(attrs, name) {
298
+ return attrs.match(new RegExp(`\\b${name}\\s*=\\s*["']([^"']+)["']`, "i"))?.[1];
299
+ }
300
+ function csprojDependencies(manifest) {
301
+ const parser = "msbuild-xml-regex", out = result(), text = fs.readFileSync(manifest.path, "utf8");
302
+ const blocks = [...text.matchAll(/<PackageReference\b([^>]*?)(?:\/>|>([\s\S]*?)<\/PackageReference>)/gi)];
303
+ for (const match of blocks) {
304
+ const name = xmlAttribute(match[1], "Include") ?? xmlAttribute(match[1], "Update");
305
+ if (!name) {
306
+ out.dependencyErrors.push(error(manifest, parser, "PackageReference missing Include or Update"));
307
+ continue;
308
+ }
309
+ const body = match[2] ?? "";
310
+ const version = xmlAttribute(match[1], "Version") ?? xmlValue(body, "Version") ?? "managed";
311
+ const privateAssets = xmlAttribute(match[1], "PrivateAssets") ?? xmlValue(body, "PrivateAssets") ?? "";
312
+ out.dependencies.push(ref(manifest, parser, 0.9, name, version, /\ball\b/i.test(privateAssets) ? "development" : "runtime"));
313
+ }
314
+ const starts = [...text.matchAll(/<PackageReference\b/gi)].length;
315
+ for (let i = blocks.length; i < starts; i++)
316
+ out.dependencyErrors.push(error(manifest, parser, "unclosed PackageReference element"));
317
+ return out;
318
+ }
319
+ export function parseManifestDependencies(manifests) {
320
+ const out = result();
321
+ for (const manifest of manifests) {
322
+ const base = path.basename(manifest.path).toLowerCase();
323
+ let parsed = null;
324
+ try {
325
+ if (manifest.kind === "node")
326
+ parsed = nodeDependencies(manifest);
327
+ else if (manifest.kind === "python")
328
+ parsed = base === "pyproject.toml" ? pyprojectDependencies(manifest) : requirementsDependencies(manifest);
329
+ else if (manifest.kind === "go")
330
+ parsed = goDependencies(manifest);
331
+ else if (manifest.kind === "rust")
332
+ parsed = cargoDependencies(manifest);
333
+ else if (manifest.kind === "maven")
334
+ parsed = mavenDependencies(manifest);
335
+ else if (manifest.kind === "gradle" && /^build\.gradle(?:\.kts)?$/i.test(base))
336
+ parsed = gradleDependencies(manifest);
337
+ else if (manifest.kind === "dotnet" && base.endsWith(".csproj"))
338
+ parsed = csprojDependencies(manifest);
339
+ }
340
+ catch (cause) {
341
+ out.dependencyErrors.push(error(manifest, `${manifest.kind}-manifest`, `manifest parse failed: ${cause instanceof Error ? cause.message : String(cause)}`));
342
+ }
343
+ if (parsed) {
344
+ out.dependencies.push(...parsed.dependencies);
345
+ out.dependencyErrors.push(...parsed.dependencyErrors);
346
+ }
347
+ }
348
+ out.dependencies.sort((a, b) => `${a.sourceManifest}\0${a.name}\0${a.scope}`.localeCompare(`${b.sourceManifest}\0${b.name}\0${b.scope}`));
349
+ out.dependencyErrors.sort((a, b) => `${a.sourceManifest}\0${a.line ?? 0}\0${a.message}`.localeCompare(`${b.sourceManifest}\0${b.line ?? 0}\0${b.message}`));
350
+ return out;
351
+ }
@@ -0,0 +1,261 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { loadFileNotes, loadProject } from "./store.js";
4
+ import { isTestFile, readDirectDeps } from "./scan.js";
5
+ import { ensureFresh, getIndex, iterateFileRows } from "./index-store.js";
6
+ import { readText } from "./search.js";
7
+ import { foldLines } from "./budget.js";
8
+ /**
9
+ * 法律/许可证账:依赖许可证清单(直接+传递,含 pnpm 布局)、copyleft 冲突、可疑许可证头、来源登记。
10
+ * 离线能力边界:读本地依赖元数据与源码文本,不做外部相似度检索。
11
+ */
12
+ const STRONG_COPYLEFT = ["GPL-2.0", "GPL-3.0", "AGPL-3.0", "SSPL-1.0", "GPL", "AGPL", "SSPL"];
13
+ const WEAK_COPYLEFT = ["LGPL", "MPL-2.0", "MPL", "EPL", "CDDL", "EUPL", "Sleepycat"];
14
+ const PERMISSIVE = ["MIT", "Apache-2.0", "Apache", "BSD-2-Clause", "BSD-3-Clause", "BSD", "ISC", "0BSD", "Unlicense", "Zlib", "Python-2.0", "BSL-1.0"];
15
+ /** 单个包最多登记数(巨型依赖树保护;超出部分计数上报) */
16
+ const MAX_DEPS = 800;
17
+ function classify(license) {
18
+ const l = license.trim();
19
+ if (!l)
20
+ return "unknown";
21
+ const up = l.toUpperCase();
22
+ if (STRONG_COPYLEFT.some((k) => up.includes(k.toUpperCase()))) {
23
+ // LGPL 不算强 copyleft
24
+ if (up.startsWith("LGPL"))
25
+ return "weak-copyleft";
26
+ return "strong-copyleft";
27
+ }
28
+ if (WEAK_COPYLEFT.some((k) => up.includes(k.toUpperCase())))
29
+ return "weak-copyleft";
30
+ if (PERMISSIVE.some((k) => up === k.toUpperCase() || up.startsWith(k.toUpperCase())))
31
+ return "permissive";
32
+ return "unknown";
33
+ }
34
+ function readPkgLicense(pkgFile) {
35
+ try {
36
+ const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
37
+ const license = typeof pkg.license === "string"
38
+ ? pkg.license
39
+ : Array.isArray(pkg.license)
40
+ ? pkg.license.join(" OR ")
41
+ : "";
42
+ return { version: String(pkg.version ?? ""), license };
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ /**
49
+ * 盘点已安装依赖许可证:直接 + 传递。
50
+ * 覆盖三种布局:npm/yarn 扁平与嵌套(node_modules/<a>/node_modules/<b>)、
51
+ * pnpm(node_modules/.pnpm/<name>@<ver>/node_modules/<name>,直接依赖是符号链接)。
52
+ */
53
+ function readNodeLicenses(root) {
54
+ const out = new Map();
55
+ const nm = path.join(root, "node_modules");
56
+ let truncated = 0;
57
+ if (!fs.existsSync(nm))
58
+ return { list: [], truncated: 0 };
59
+ const direct = new Set(readDirectDeps(root).deps);
60
+ const record = (name, pkgDir) => {
61
+ if (out.has(name) || out.size >= MAX_DEPS) {
62
+ if (!out.has(name))
63
+ truncated += 1;
64
+ return;
65
+ }
66
+ const meta = readPkgLicense(path.join(pkgDir, "package.json"));
67
+ out.set(name, {
68
+ name,
69
+ version: meta?.version ?? "",
70
+ license: meta?.license ?? "",
71
+ kind: classify(meta?.license ?? ""),
72
+ direct: direct.has(name),
73
+ });
74
+ };
75
+ const isDirLike = (d) => d.isDirectory() || d.isSymbolicLink();
76
+ // ① 一层 node_modules(npm/yarn 扁平 + pnpm 的符号链接直接依赖)
77
+ const level1 = [];
78
+ for (const entry of fs.readdirSync(nm, { withFileTypes: true })) {
79
+ if (entry.name === ".pnpm" || entry.name.startsWith("."))
80
+ continue;
81
+ if (entry.name.startsWith("@") && isDirLike(entry)) {
82
+ for (const sub of fs.readdirSync(path.join(nm, entry.name), { withFileTypes: true })) {
83
+ if (isDirLike(sub))
84
+ level1.push(`${entry.name}/${sub.name}`);
85
+ }
86
+ }
87
+ else if (isDirLike(entry)) {
88
+ level1.push(entry.name);
89
+ }
90
+ }
91
+ for (const name of level1)
92
+ record(name, path.join(nm, ...name.split("/")));
93
+ // ② 嵌套依赖(npm2 老布局 / 部分包自带 node_modules)
94
+ for (const name of level1) {
95
+ const nested = path.join(nm, ...name.split("/"), "node_modules");
96
+ if (!fs.existsSync(nested))
97
+ continue;
98
+ for (const entry of fs.readdirSync(nested, { withFileTypes: true })) {
99
+ if (entry.name.startsWith("@") && isDirLike(entry)) {
100
+ for (const sub of fs.readdirSync(path.join(nested, entry.name), { withFileTypes: true })) {
101
+ if (isDirLike(sub))
102
+ record(`${entry.name}/${sub.name}`, path.join(nested, entry.name, sub.name));
103
+ }
104
+ }
105
+ else if (isDirLike(entry)) {
106
+ record(entry.name, path.join(nested, entry.name));
107
+ }
108
+ }
109
+ }
110
+ // ③ pnpm 布局:.pnpm/<name>@<version>/node_modules/<name> 覆盖整个依赖树(直接+传递)
111
+ const pnpmDir = path.join(nm, ".pnpm");
112
+ if (fs.existsSync(pnpmDir)) {
113
+ for (const entry of fs.readdirSync(pnpmDir, { withFileTypes: true })) {
114
+ if (!entry.isDirectory())
115
+ continue;
116
+ // 目录名 name@version;@scope/name@version → 包名取到最后一个 @ 前
117
+ const dirName = entry.name;
118
+ const at = dirName.lastIndexOf("@");
119
+ if (at <= 0)
120
+ continue;
121
+ const pkgName = dirName.slice(0, at);
122
+ if (!pkgName)
123
+ continue;
124
+ const realDir = path.join(pnpmDir, dirName, "node_modules", ...pkgName.split("/"));
125
+ if (fs.existsSync(path.join(realDir, "package.json")))
126
+ record(pkgName, realDir);
127
+ }
128
+ }
129
+ const list = [...out.values()].sort((a, b) => a.name.localeCompare(b.name));
130
+ return { list, truncated };
131
+ }
132
+ const GPL_HEADER = /GNU (LESSER |AFFERO )?GENERAL PUBLIC LICENSE/i;
133
+ export function auditLicense(root, maxLines = 150) {
134
+ const project = loadProject(root);
135
+ const rootPkg = path.join(root, "package.json");
136
+ let pkgLicense = "";
137
+ if (fs.existsSync(rootPkg)) {
138
+ try {
139
+ pkgLicense = String(JSON.parse(fs.readFileSync(rootPkg, "utf8")).license ?? "");
140
+ }
141
+ catch {
142
+ /* ignore */
143
+ }
144
+ }
145
+ const declared = project.license || pkgLicense;
146
+ const projectKind = classify(declared);
147
+ const L = [];
148
+ L.push("## 许可证审计");
149
+ /* ① 项目声明 */
150
+ L.push("");
151
+ L.push("### ① 项目声明");
152
+ const hasLicenseFile = fs.existsSync(root) &&
153
+ fs.readdirSync(root).some((n) => /^licen[cs]e/i.test(n));
154
+ L.push(declared
155
+ ? `- 项目许可证: ${declared}(${label(projectKind)})`
156
+ : "- ⚠️ 项目未声明许可证(update_project 补充 license 字段)");
157
+ L.push(hasLicenseFile ? "- LICENSE 文件: 存在" : "- ⚠️ 根目录缺少 LICENSE 文件");
158
+ /* ② 依赖清单(直接 + 传递,含 pnpm 布局) */
159
+ L.push("");
160
+ const { list: licenses, truncated } = readNodeLicenses(root);
161
+ const directList = licenses.filter((d) => d.direct);
162
+ const transitiveList = licenses.filter((d) => !d.direct);
163
+ L.push(`### ② 依赖许可证清单(直接 ${directList.length} + 传递 ${transitiveList.length})`);
164
+ const conflicts = [];
165
+ const unknowns = [];
166
+ const emit = (d) => {
167
+ const tag = d.direct ? "" : "(传递)";
168
+ if (d.kind === "strong-copyleft") {
169
+ conflicts.push(`🔴 ${d.name}@${d.version} [${d.license || "?"}] 强 copyleft${tag}`);
170
+ }
171
+ else if (d.kind === "unknown") {
172
+ unknowns.push(d.name + tag);
173
+ }
174
+ else if (d.kind === "weak-copyleft") {
175
+ L.push(`🟡 ${d.name}@${d.version} [${d.license}] 弱 copyleft(注意按文件级义务使用)${tag}`);
176
+ }
177
+ else {
178
+ L.push(`🟢 ${d.name}@${d.version} [${d.license}]${tag}`);
179
+ }
180
+ };
181
+ if (licenses.length === 0) {
182
+ L.push("- (未发现已安装的 JS 依赖)");
183
+ }
184
+ for (const d of directList)
185
+ emit(d);
186
+ for (const d of transitiveList.slice(0, 60))
187
+ emit(d);
188
+ if (transitiveList.length > 60)
189
+ L.push(`…另有 ${transitiveList.length - 60} 个传递依赖未逐条列出(详见 node_modules)`);
190
+ if (truncated > 0)
191
+ L.push(`⚪ 依赖树超过 ${MAX_DEPS} 个,${truncated} 个未逐条登记`);
192
+ for (const c of conflicts)
193
+ L.push(c);
194
+ if (unknowns.length > 0)
195
+ L.push(`⚪ 许可证未知(需人工审查): ${unknowns.slice(0, 20).join(", ")}`);
196
+ const declaredDeps = readDirectDeps(root).deps;
197
+ const foundNames = new Set(licenses.map((l) => l.name));
198
+ const missing = declaredDeps.filter((n) => !foundNames.has(n));
199
+ if (missing.length > 0) {
200
+ const hasPy = fs.existsSync(path.join(root, "requirements.txt"));
201
+ L.push(`⚪ 已声明但未在 node_modules 找到 ${missing.length} 个(未安装或元数据缺失${hasPy ? ";Python 依赖离线无法取许可证,需人工审查" : ""}): ${missing.slice(0, 10).join(", ")}`);
202
+ }
203
+ if (conflicts.length > 0 && (projectKind === "permissive" || declared === "")) {
204
+ L.push("");
205
+ L.push(`- 🚩 项目按 ${declared || "专有"} 分发,但存在强 copyleft 依赖(含传递)。若闭源/专有分发,GPL/AGPL/SSPL 依赖会触发开源义务——请确认链接方式或替换依赖,必要时咨询法务。`);
206
+ }
207
+ /* ③ 可疑许可证头 */
208
+ L.push("");
209
+ L.push("### ③ 源码中的 GPL 家族许可证头(复制带许可证代码的常见痕迹)");
210
+ ensureFresh(root);
211
+ const hits = [];
212
+ for (const row of iterateFileRows(getIndex(root))) {
213
+ const rel = row.rel;
214
+ if (rel.startsWith(".pm/"))
215
+ continue;
216
+ // 测试文件常内嵌许可证字符串作为 fixture,不参与该启发式(信号针对混入源码的复制代码)
217
+ if (isTestFile(rel))
218
+ continue;
219
+ const content = readText(root, rel);
220
+ if (content === null)
221
+ continue;
222
+ if (GPL_HEADER.test(content.split("\n").slice(0, 40).join("\n")))
223
+ hits.push(rel);
224
+ if (hits.length >= 10)
225
+ break;
226
+ }
227
+ if (hits.length === 0)
228
+ L.push("- ✅ 未在前 40 行发现 GPL 家族许可证头。");
229
+ else {
230
+ for (const h of hits)
231
+ L.push(`- 🔴 ${h}:包含 GPL 家族许可证文本,疑似复制自 copyleft 项目——确认来源与义务,或用 annotate_file 登记来源。`);
232
+ }
233
+ /* ④ 来源登记 */
234
+ L.push("");
235
+ L.push("### ④ 来源登记(provenance)");
236
+ const notes = loadFileNotes(root).notes;
237
+ const sourced = Object.entries(notes).filter(([, n]) => n.source || n.license);
238
+ if (sourced.length === 0) {
239
+ L.push("- (暂无来源登记。引用外部代码时用 annotate_file 登记 source 与 license,纠纷时说得清代码从哪来。)");
240
+ }
241
+ else {
242
+ for (const [file, n] of sourced.slice(0, 10)) {
243
+ L.push(`- ${file}: ${n.source || "未填来源"}${n.license ? ` [${n.license}]` : ""}`);
244
+ }
245
+ }
246
+ L.push("");
247
+ L.push("> 边界说明:本审计是离线启发式(依赖元数据 + 文本头检测),不能检测「AI 复读受版权代码」本身,也不替代法务意见。");
248
+ return foldLines(L, { maxLines, hint: "冲突项优先,可用 update_project 声明许可证后重扫" });
249
+ }
250
+ function label(kind) {
251
+ switch (kind) {
252
+ case "strong-copyleft":
253
+ return "强 copyleft";
254
+ case "weak-copyleft":
255
+ return "弱 copyleft";
256
+ case "permissive":
257
+ return "宽松";
258
+ default:
259
+ return "未知";
260
+ }
261
+ }
package/dist/paths.js ADDED
@@ -0,0 +1,51 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ /** 项目根解析顺序:--root 参数 > PM_ROOT 环境变量 > 当前工作目录 */
5
+ export function resolveRoot(explicit) {
6
+ const raw = explicit?.trim() || process.env.PM_ROOT?.trim() || process.cwd();
7
+ return path.resolve(raw);
8
+ }
9
+ export const PM_DIR = ".pm";
10
+ export function pmPath(root, ...segs) {
11
+ return path.join(root, PM_DIR, ...segs);
12
+ }
13
+ export const PROJECT_JSON = "project.json";
14
+ export const ROADMAP_JSON = "roadmap.json";
15
+ export const TASKS_JSON = "tasks.json";
16
+ export const FEATURES_JSON = "features.json";
17
+ export const SESSIONS_JSON = "sessions.json";
18
+ export const DEBUGLOG_JSON = "debuglog.json";
19
+ export const FILE_NOTES_JSON = "file-notes.json";
20
+ export const SECURITY_JSON = "security.json";
21
+ export const DECISIONS_DIR = "decisions";
22
+ export const SNAPSHOTS_DIR = "snapshots";
23
+ export const EXTRA_RULES_FILE = "security-rules.json";
24
+ export function decisionsDir(root) {
25
+ return pmPath(root, DECISIONS_DIR);
26
+ }
27
+ export function snapshotsDir(root) {
28
+ return pmPath(root, SNAPSHOTS_DIR);
29
+ }
30
+ /** 仓库根目录下自动生成的仪表盘 */
31
+ export function dashboardFile(root) {
32
+ return path.join(root, "PROJECT.md");
33
+ }
34
+ /** 全局项目注册表(跨项目查询用);PM_MCP_HOME 可重定向(测试/多环境隔离用) */
35
+ export function registryFile() {
36
+ const home = process.env.PM_MCP_HOME?.trim() || os.homedir();
37
+ return path.join(home, ".pm-mcp", "registry.json");
38
+ }
39
+ export function isInitialized(root) {
40
+ return fs.existsSync(pmPath(root, PROJECT_JSON));
41
+ }
42
+ export function requireInitialized(root) {
43
+ if (!isInitialized(root)) {
44
+ throw new Error(`项目未初始化(在 ${root} 下没有找到 ${PM_DIR}/${PROJECT_JSON})。请先调用 init_project。`);
45
+ }
46
+ }
47
+ export function ensurePmDirs(root) {
48
+ fs.mkdirSync(pmPath(root), { recursive: true });
49
+ fs.mkdirSync(decisionsDir(root), { recursive: true });
50
+ fs.mkdirSync(snapshotsDir(root), { recursive: true });
51
+ }