@luckychen1993/pm-mcp 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/doctor.js CHANGED
@@ -5,7 +5,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
5
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
6
6
  import { PACKAGE_SPEC, RUNTIME_CONTRACT, SERVER_NAME, TOOL_CATALOG_SIZE, VERSION } from "./version.js";
7
7
  import { canonicalProjectRoot, findCodexProjectEntry, isLocalLaunch, isLocalServerEntry, isPackageLaunch, readCodexConfig } from "./setup-project.js";
8
- import { localProjectLaunch, workbuddyConfigPath } from "./setup.js";
8
+ import { localProjectLaunch, qoderConfigPath, workbuddyConfigPath } from "./setup.js";
9
9
  export function parseDoctorArgs(argv) {
10
10
  const options = { root: process.cwd() };
11
11
  for (let index = 0; index < argv.length; index++) {
@@ -19,8 +19,8 @@ export function parseDoctorArgs(argv) {
19
19
  if (arg === "--config")
20
20
  options.config = path.resolve(value);
21
21
  if (arg === "--client") {
22
- if (value !== "codex" && value !== "workbuddy")
23
- throw new Error("doctor 支持 --client codex|workbuddy,其他客户端可用 --entry 验证本地入口。");
22
+ if (value !== "codex" && value !== "workbuddy" && value !== "qoder")
23
+ throw new Error("doctor 支持 --client codex|workbuddy|qoder,其他客户端可用 --entry 验证本地入口。");
24
24
  options.client = value;
25
25
  }
26
26
  index++;
@@ -30,35 +30,35 @@ export function parseDoctorArgs(argv) {
30
30
  return options;
31
31
  }
32
32
  /**
33
- * 从 WorkBuddy 的 `mcp.json` 中取出 pm-mcp 条目。
33
+ * 从标准 `{ "mcpServers": { ... } }` 配置文件(WorkBuddy 的 mcp.json、Qoder 的 settings.json)中取出 pm-mcp 条目。
34
34
  *
35
35
  * 只读取启动定义(command/args),不复制也不输出任何自定义 env / 凭据。
36
36
  * 文件必须是**有界普通文件**:链接、目录、超 2MB 一律拒绝。
37
37
  */
38
- function readWorkbuddyEntry(file) {
38
+ function readJsonMcpEntry(file, label) {
39
39
  if (!fs.existsSync(file))
40
40
  return undefined;
41
41
  const stat = fs.lstatSync(file);
42
42
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 2 * 1024 * 1024)
43
- throw new Error("WorkBuddy 配置必须为有界普通文件,禁止链接。");
43
+ throw new Error(`${label} 配置必须为有界普通文件,禁止链接。`);
44
44
  let parsed;
45
45
  try {
46
46
  parsed = JSON.parse(fs.readFileSync(file, "utf8"));
47
47
  }
48
48
  catch (error) {
49
- throw new Error(`WorkBuddy 配置不是合法 JSON:${error.message}`);
49
+ throw new Error(`${label} 配置不是合法 JSON:${error.message}`);
50
50
  }
51
51
  const servers = parsed?.mcpServers;
52
52
  const entry = servers?.[SERVER_NAME];
53
53
  if (!entry)
54
54
  return undefined;
55
55
  if (entry.disabled === true)
56
- throw new Error("WorkBuddy 中 pm-mcp 条目被禁用,现有客户端不会启动它。");
56
+ throw new Error(`${label} 中 pm-mcp 条目被禁用,现有客户端不会启动它。`);
57
57
  if (typeof entry.command !== "string" || entry.command.length === 0)
58
- throw new Error("WorkBuddy 中 pm-mcp 条目缺少可识别的 command。");
58
+ throw new Error(`${label} 中 pm-mcp 条目缺少可识别的 command。`);
59
59
  const args = entry.args === undefined ? [] : entry.args;
60
60
  if (!Array.isArray(args) || !args.every((value) => typeof value === "string"))
61
- throw new Error("WorkBuddy 中 pm-mcp 条目的 args 不是字符串数组。");
61
+ throw new Error(`${label} 中 pm-mcp 条目的 args 不是字符串数组。`);
62
62
  return { command: entry.command, args: args };
63
63
  }
64
64
  /** 短期本地读连接;不把探测新进程冒充已重新连接的客户端。 */
@@ -72,17 +72,20 @@ export async function runDoctor(options) {
72
72
  try {
73
73
  if (options.entry)
74
74
  launch = { command: process.execPath, args: [options.entry, "--root", root] };
75
- else if (options.client === "workbuddy") {
76
- const file = options.config ?? workbuddyConfigPath(process.env, os.homedir(), fs.existsSync);
77
- const entry = readWorkbuddyEntry(file);
75
+ else if (options.client === "workbuddy" || options.client === "qoder") {
76
+ const label = options.client === "qoder" ? "Qoder" : "WorkBuddy";
77
+ const file = options.config ?? (options.client === "qoder"
78
+ ? qoderConfigPath(process.env, os.homedir(), fs.existsSync)
79
+ : workbuddyConfigPath(process.env, os.homedir(), fs.existsSync));
80
+ const entry = readJsonMcpEntry(file, label);
78
81
  if (!entry)
79
- throw new Error("未在 WorkBuddy 的 mcp.json 中找到 pm-mcp 条目。");
82
+ throw new Error(`未在 ${label} 的 ${path.basename(file)} 中找到 pm-mcp 条目。`);
80
83
  report.configured_server = SERVER_NAME;
81
84
  if (!isLocalServerEntry(entry))
82
- throw new Error("WorkBuddy 中的 pm-mcp 不是可识别的本地 pm-mcp 启动项;未执行未知启动命令。");
85
+ throw new Error(`${label} 中的 pm-mcp 不是可识别的本地 pm-mcp 启动项;未执行未知启动命令。`);
83
86
  // 配置通常不钉根(由会话工作区决定);探测需要明确目标根,故补上 --root。
84
87
  launch = entry.args.includes("--root") ? entry : { command: entry.command, args: [...entry.args, "--root", root] };
85
- report.configuration = "workbuddy_local_probe";
88
+ report.configuration = `${options.client}_local_probe`;
86
89
  }
87
90
  else {
88
91
  if (!options.config && fs.existsSync(path.join(root, ".codex", "config.toml")))
package/dist/git-state.js CHANGED
@@ -4,6 +4,7 @@ import { createHash } from "node:crypto";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { normSep } from "./budget.js";
6
6
  import { hashRegularFile } from "./project-fingerprint.js";
7
+ import { ignoreMatcher, projectScanIgnores } from "./scan-policy.js";
7
8
  export function projectFilePath(relative) {
8
9
  const rel = path.posix.normalize(normSep(relative));
9
10
  if (!relative || path.isAbsolute(rel) || /^[a-z]:/i.test(rel) || rel === "." || rel === ".." || rel.startsWith("../")) {
@@ -11,16 +12,54 @@ export function projectFilePath(relative) {
11
12
  }
12
13
  return rel;
13
14
  }
15
+ let gitCommandCache;
16
+ /** PATH 缺 git 的宿主进程(对账 fail-closed 恒红)按常见安装路径兜底解析并缓存。 */
17
+ export function resolveGitCommand() {
18
+ if (gitCommandCache !== undefined)
19
+ return gitCommandCache;
20
+ const probe = (command) => spawnSync(command, ["--version"], { encoding: "utf8", timeout: 5000, windowsHide: true }).status === 0;
21
+ if (probe("git")) {
22
+ gitCommandCache = "git";
23
+ }
24
+ else if (process.platform === "win32") {
25
+ const where = spawnSync("where", ["git"], { encoding: "utf8", timeout: 5000, windowsHide: true });
26
+ const located = where.status === 0 ? where.stdout.split(/\r?\n/).find(Boolean)?.trim() : undefined;
27
+ const programFiles = process.env["ProgramFiles"] ?? "C:\\Program Files";
28
+ const candidates = [
29
+ ...(located ? [located] : []),
30
+ path.join(programFiles, "Git", "cmd", "git.exe"),
31
+ "C:\\Program Files (x86)\\Git\\cmd\\git.exe",
32
+ path.join(process.env["LOCALAPPDATA"] ?? "", "Programs", "Git", "cmd", "git.exe"),
33
+ ];
34
+ gitCommandCache = candidates.find((candidate) => candidate && fs.existsSync(candidate) && probe(candidate)) ?? null;
35
+ }
36
+ else {
37
+ gitCommandCache = ["/usr/bin/git", "/usr/local/bin/git", "/opt/homebrew/bin/git"].find((candidate) => probe(candidate)) ?? null;
38
+ }
39
+ return gitCommandCache;
40
+ }
41
+ /** 仅测试用:清空解析缓存,模拟不同 PATH 环境。 */
42
+ export function resetGitCommandCache() {
43
+ gitCommandCache = undefined;
44
+ }
14
45
  /** 使用 NUL 分隔,避免中文转义、空格及文件名中的箭头被误解析。 */
15
46
  export function changedGitFiles(root) {
16
- const result = spawnSync("git", ["status", "--porcelain=v1", "-z", "-uall"], { cwd: root, encoding: "utf8", timeout: 10000, maxBuffer: 16 * 1024 * 1024 });
47
+ const excluded = ignoreMatcher(projectScanIgnores(root));
48
+ const git = resolveGitCommand();
49
+ if (!git)
50
+ throw new Error("找不到 git 可执行文件(PATH 与常见安装路径均未命中),git 对账不可用");
51
+ const result = spawnSync(git, ["status", "--porcelain=v1", "-z", "-uall"], { cwd: root, encoding: "utf8", timeout: 10000, maxBuffer: 16 * 1024 * 1024 });
17
52
  if (result.error || result.status !== 0)
18
53
  throw new Error(result.error?.message ?? `git status 退出码 ${result.status}`);
19
54
  const records = result.stdout.split("\0");
20
55
  const changed = new Set();
21
56
  const add = (file) => {
22
57
  const rel = projectFilePath(file);
23
- if (!rel.startsWith(".pm/") && rel !== "PROJECT.md" && rel !== ".gitignore")
58
+ const parts = rel.split("/");
59
+ if (!rel.startsWith(".pm/") &&
60
+ rel !== "PROJECT.md" &&
61
+ rel !== ".gitignore" &&
62
+ !parts.some((_, index) => excluded(parts.slice(0, index + 1).join("/"), index < parts.length - 1)))
24
63
  changed.add(rel);
25
64
  };
26
65
  for (let i = 0; i < records.length; i++) {
@@ -101,6 +101,16 @@ function render(governance, graph, units, qualityCoverage, issues, maxLines) {
101
101
  lines.push(`- 🚩 未读取/超限文件样例: ${graph.coverage.skippedFiles.slice(0, 8).join(", ")}`);
102
102
  return foldLines(lines, { maxLines, hint: "用 impact_analysis 或治理过滤参数缩小范围" });
103
103
  }
104
+ export function listGovernanceIssues(root, options = {}) {
105
+ const audit = auditGovernance(root, Number.MAX_SAFE_INTEGER);
106
+ const offset = Math.max(0, Math.trunc(options.offset ?? 0));
107
+ const limit = Math.min(100, Math.max(1, Math.trunc(options.limit ?? 50)));
108
+ const filtered = audit.issues.filter((issue) => (options.severity === undefined || options.severity === "all" || issue.severity === options.severity) &&
109
+ (!options.code || options.code === "all" || issue.code === options.code));
110
+ const items = filtered.slice(offset, offset + limit);
111
+ const nextOffset = offset + items.length < filtered.length ? offset + items.length : null;
112
+ return { total: filtered.length, offset, limit, items, nextOffset };
113
+ }
104
114
  export function auditGovernance(root, maxLines = 150) {
105
115
  const absolute = path.resolve(root);
106
116
  const governance = loadGovernance(absolute);
@@ -5,7 +5,7 @@ import { foldLines } from "./budget.js";
5
5
  import { refreshDerived } from "./dashboard.js";
6
6
  import { requireInitialized } from "./paths.js";
7
7
  import { initGovernance, loadGovernance, saveGovernance, } from "./governance-model.js";
8
- import { auditGovernance, dependencyGraphReport } from "./governance-audit.js";
8
+ import { auditGovernance, dependencyGraphReport, listGovernanceIssues } from "./governance-audit.js";
9
9
  import { impactAnalysis } from "./semantic-graph.js";
10
10
  import { assessQualityCoverage, createQualityPlan, discoverProjectUnits, runQualityPlan } from "./language-adapters.js";
11
11
  import { buildPortfolioFromRegistry, buildPortfolioReport, loadPortfolioProject } from "./portfolio.js";
@@ -64,18 +64,21 @@ function qualityPlan(root, unit, kinds) {
64
64
  function qualityLines(commands) {
65
65
  return commands.map((command, index) => `${index + 1}. [${command.kind}] (${command.cwd}) ${command.command} ${command.args.join(" ")}`);
66
66
  }
67
- export function registerGovernanceTools(server, root) {
68
- toolI(server, root, "init_governance", "为旧项目初始化 .pm/governance.json;新项目 init_project 已自动创建。", {}, () => {
67
+ export function registerGovernanceTools(server, startupRoot) {
68
+ toolI(server, startupRoot, "init_governance", "为旧项目初始化 .pm/governance.json;新项目 init_project 已自动创建。", {}, (_args, ctx) => {
69
+ const root = ctx.root;
69
70
  requireInitialized(root);
70
71
  const governance = initGovernance(root, {});
71
72
  refreshDerived(root);
72
73
  return `✅ 治理模型已初始化(schema v${governance.schema_version})。下一步用 upsert_module 声明模块、owner 与语言。`;
73
74
  });
74
- toolR(server, root, "get_governance", "查看结构化模块、owner、公开接口、仓库依赖与强制策略。", {}, () => {
75
+ toolR(server, startupRoot, "get_governance", "查看结构化模块、owner、公开接口、仓库依赖与强制策略。", {}, (_args, ctx) => {
76
+ const root = ctx.root;
75
77
  requireInitialized(root);
76
78
  return compactGovernance(loadGovernance(root));
77
79
  });
78
- toolW(server, root, "upsert_module", "新增或完整替换一个治理模块;root/owner/language/依赖边均经 schema 对账。", moduleInput, (args) => {
80
+ toolW(server, startupRoot, "upsert_module", "新增或完整替换一个治理模块;root/owner/language/依赖边均经 schema 对账。", moduleInput, (args, ctx) => {
81
+ const root = ctx.root;
79
82
  requireInitialized(root);
80
83
  const governance = loadGovernance(root);
81
84
  const value = {
@@ -90,14 +93,15 @@ export function registerGovernanceTools(server, root) {
90
93
  refreshDerived(root);
91
94
  return `✅ module ${value.id} 已保存(roots ${value.roots.length},owners ${value.owners.length},languages ${value.languages.length})。`;
92
95
  });
93
- toolW(server, root, "upsert_interface", "新增或替换跨模块公开接口及契约文件、provider/consumers、版本。", {
96
+ toolW(server, startupRoot, "upsert_interface", "新增或替换跨模块公开接口及契约文件、provider/consumers、版本。", {
94
97
  id,
95
98
  kind: z.string().trim().min(1),
96
99
  provider: id,
97
100
  consumers: texts,
98
101
  contract_files: texts,
99
102
  version: z.string().trim().min(1),
100
- }, (args) => {
103
+ }, (args, ctx) => {
104
+ const root = ctx.root;
101
105
  requireInitialized(root);
102
106
  const governance = loadGovernance(root);
103
107
  if (!governance.modules.some((module) => module.id === args.provider))
@@ -110,13 +114,14 @@ export function registerGovernanceTools(server, root) {
110
114
  refreshDerived(root);
111
115
  return `✅ interface ${args.id}@${args.version} 已保存:${args.provider} -> ${args.consumers.join(",") || "无消费者"}。`;
112
116
  });
113
- toolW(server, root, "upsert_repository", "登记当前组合视图中的仓库、版本及跨仓 semver 约束。", {
117
+ toolW(server, startupRoot, "upsert_repository", "登记当前组合视图中的仓库、版本及跨仓 semver 约束。", {
114
118
  id,
115
119
  name: z.string().trim().min(1),
116
120
  root: z.string().trim().min(1),
117
121
  version: z.string().trim().min(1),
118
122
  dependencies: z.array(z.object({ repository: id, constraint: z.string().trim().min(1) })),
119
- }, (args) => {
123
+ }, (args, ctx) => {
124
+ const root = ctx.root;
120
125
  requireInitialized(root);
121
126
  const governance = loadGovernance(root);
122
127
  replaceById(governance.repositories, args);
@@ -124,7 +129,7 @@ export function registerGovernanceTools(server, root) {
124
129
  refreshDerived(root);
125
130
  return `✅ repository ${args.id}@${args.version} 已保存(依赖 ${args.dependencies.length})。`;
126
131
  });
127
- toolW(server, root, "set_governance_policies", "更新治理强制策略;未传字段保持原值。", {
132
+ toolW(server, startupRoot, "set_governance_policies", "更新治理强制策略;未传字段保持原值。", {
128
133
  enforce_ownership: z.boolean().optional(),
129
134
  enforce_declared_dependencies: z.boolean().optional(),
130
135
  fail_on_unresolved: z.boolean().optional(),
@@ -133,7 +138,8 @@ export function registerGovernanceTools(server, root) {
133
138
  minimum_semantic_assurance: z.enum(["heuristic", "ast", "runtime"]).optional(),
134
139
  fail_on_semantic_fallback: z.boolean().optional(),
135
140
  required_quality_kinds: z.array(z.enum(["test", "build", "lint", "typecheck", "coverage", "security"])).optional(),
136
- }, (args) => {
141
+ }, (args, ctx) => {
142
+ const root = ctx.root;
137
143
  requireInitialized(root);
138
144
  const governance = loadGovernance(root);
139
145
  governance.policies = { ...governance.policies, ...args };
@@ -141,7 +147,8 @@ export function registerGovernanceTools(server, root) {
141
147
  refreshDerived(root);
142
148
  return `✅ 治理策略已更新:coverage>=${governance.policies.minimum_coverage_pct}% · semantic>=${governance.policies.minimum_semantic_assurance} · regex-fallback=${governance.policies.fail_on_semantic_fallback ? "forbidden" : "allowed"}`;
143
149
  });
144
- toolR(server, root, "discover_languages", "递归发现 Node/Python/Go/Rust/JVM/.NET 单元及其受控质量命令,不执行命令。", {}, () => {
150
+ toolR(server, startupRoot, "discover_languages", "递归发现 Node/Python/Go/Rust/JVM/.NET 单元及其受控质量命令,不执行命令。", {}, (_args, ctx) => {
151
+ const root = ctx.root;
145
152
  requireInitialized(root);
146
153
  const units = discoverProjectUnits(root);
147
154
  const coverage = assessQualityCoverage(units);
@@ -152,41 +159,57 @@ export function registerGovernanceTools(server, root) {
152
159
  lines.push(`- 🚩 无质量命令: ${coverage.withoutCommands.join(", ")}`);
153
160
  return foldLines(lines, { maxLines: budgetLines(root), hint: "按模块拆分治理 root" });
154
161
  });
155
- toolR(server, root, "audit_governance", "审计跨文件/模块/语言语义覆盖、owner、公开接口、依赖边界、循环、unresolved 与质量矩阵覆盖。", {}, () => {
162
+ toolR(server, startupRoot, "audit_governance", "审计跨文件/模块/语言语义覆盖、owner、公开接口、依赖边界、循环、unresolved 与质量矩阵覆盖。", {}, (_args, ctx) => {
163
+ const root = ctx.root;
156
164
  requireInitialized(root);
157
165
  return auditGovernance(root, budgetLines(root)).report;
158
166
  });
159
- toolR(server, root, "dependency_graph", "查询跨文件/模块依赖图:全局摘要(关系/环/入出边枢纽)、模块与文件级循环、聚焦文件的依赖邻域(BFS 方向与深度可调、未解析引用)。", {
167
+ toolR(server, startupRoot, "list_governance_issues", "分页读取完整治理审计问题,不受 audit_governance 文本预算折叠影响;只读且按 offset/limit 返回。", {
168
+ offset: z.number().int().min(0).optional().describe("起始偏移,默认 0"),
169
+ limit: z.number().int().min(1).max(100).optional().describe("每页数量,默认 50"),
170
+ severity: z.enum(["all", "error", "warning"]).optional().describe("按级别过滤"),
171
+ code: z.string().trim().max(100).optional().describe("按问题 code 过滤"),
172
+ }, (args, ctx) => {
173
+ const root = ctx.root;
174
+ requireInitialized(root);
175
+ return JSON.stringify(listGovernanceIssues(root, args ?? {}), null, 2);
176
+ });
177
+ toolR(server, startupRoot, "dependency_graph", "查询跨文件/模块依赖图:全局摘要(关系/环/入出边枢纽)、模块与文件级循环、聚焦文件的依赖邻域(BFS 方向与深度可调、未解析引用)。", {
160
178
  focus: z.string().trim().min(1).max(512).optional().describe("聚焦文件(相对项目根路径)"),
161
179
  direction: z.enum(["deps", "dependents", "both"]).optional().describe("邻域方向,默认 both"),
162
180
  depth: z.number().int().min(1).max(6).optional().describe("邻域深度 1-6,默认 2"),
163
- }, (args) => {
181
+ }, (args, ctx) => {
182
+ const root = ctx.root;
164
183
  requireInitialized(root);
165
184
  return dependencyGraphReport(root, args ?? {}, budgetLines(root));
166
185
  });
167
- toolR(server, root, "list_semantic_evidence", "列出已登记的语言原生 AST、编译器或运行时语义证据;损坏文档会使整次读取失败。", {}, () => {
186
+ toolR(server, startupRoot, "list_semantic_evidence", "列出已登记的语言原生 AST、编译器或运行时语义证据;损坏文档会使整次读取失败。", {}, (_args, ctx) => {
187
+ const root = ctx.root;
168
188
  requireInitialized(root);
169
189
  return JSON.stringify(listSemanticEvidence(root), null, 2);
170
190
  });
171
- toolI(server, root, "replace_semantic_evidence_for_file", "按源码文件原子替换语义证据:删除该文件旧 evidence,仅保留当前 hash-bound evidence。", {
191
+ toolI(server, startupRoot, "replace_semantic_evidence_for_file", "按源码文件原子替换语义证据:删除该文件旧 evidence,仅保留当前 hash-bound evidence。", {
172
192
  id: z.string().trim().min(1).max(128),
173
193
  document: SemanticEvidenceDocumentSchema,
174
- }, (args) => {
194
+ }, (args, ctx) => {
195
+ const root = ctx.root;
175
196
  requireInitialized(root);
176
197
  const saved = replaceSemanticEvidenceForFile(root, args.id, args.document);
177
198
  return `✅ semantic evidence replaced ${args.id}:${saved.file} · ${saved.analyzer.id} · ${saved.analyzer.assurance} · ${saved.status}`;
178
199
  });
179
- toolI(server, root, "save_semantic_evidence", "保存一份绑定源码 SHA-256 的语言原生 AST/编译器/运行时证据;过期摘要在治理审计中严格失败。", {
200
+ toolI(server, startupRoot, "save_semantic_evidence", "保存一份绑定源码 SHA-256 的语言原生 AST/编译器/运行时证据;过期摘要在治理审计中严格失败。", {
180
201
  id: z.string().trim().min(1).max(128),
181
202
  document: SemanticEvidenceDocumentSchema,
182
- }, (args) => {
203
+ }, (args, ctx) => {
204
+ const root = ctx.root;
183
205
  requireInitialized(root);
184
206
  const saved = saveSemanticEvidence(root, args.id, args.document);
185
207
  return `✅ semantic evidence ${args.id} 已保存:${saved.file} · ${saved.analyzer.id} · ${saved.analyzer.assurance} · ${saved.status}`;
186
208
  });
187
- toolR(server, root, "impact_analysis", "从变更文件沿 file/module 反向依赖闭包计算受影响文件与模块。", {
209
+ toolR(server, startupRoot, "impact_analysis", "从变更文件沿 file/module 反向依赖闭包计算受影响文件与模块。", {
188
210
  files: z.array(z.string().min(1)).min(1).max(100),
189
- }, (args) => {
211
+ }, (args, ctx) => {
212
+ const root = ctx.root;
190
213
  requireInitialized(root);
191
214
  const audit = auditGovernance(root, budgetLines(root));
192
215
  const impact = impactAnalysis(audit.graph, args.files);
@@ -197,10 +220,11 @@ export function registerGovernanceTools(server, root) {
197
220
  ...(impact.unknownChangedFiles.length ? [`⚠️ 未进入语义图: ${impact.unknownChangedFiles.join(", ")}`] : []),
198
221
  ], { maxLines: budgetLines(root), hint: "缩小 files 输入或按模块运行" });
199
222
  });
200
- toolR(server, root, "plan_quality_matrix", "生成跨语言真实质量命令计划;只规划,不启动进程。", {
223
+ toolR(server, startupRoot, "plan_quality_matrix", "生成跨语言真实质量命令计划;只规划,不启动进程。", {
201
224
  unit: z.string().optional(),
202
225
  kinds: z.array(z.enum(["test", "build", "lint", "typecheck", "coverage", "security"])).optional(),
203
- }, (args) => {
226
+ }, (args, ctx) => {
227
+ const root = ctx.root;
204
228
  requireInitialized(root);
205
229
  const plan = qualityPlan(root, args.unit, args.kinds);
206
230
  return foldLines([
@@ -209,12 +233,13 @@ export function registerGovernanceTools(server, root) {
209
233
  ...(plan.coverage.withoutCommands.length ? [`🚩 无命令单元: ${plan.coverage.withoutCommands.join(", ")}`] : []),
210
234
  ], { maxLines: budgetLines(root), hint: "用 unit/kinds 过滤;真实执行需 run_quality_matrix confirm_execute=true" });
211
235
  });
212
- toolI(server, root, "run_quality_matrix", "显式执行跨语言质量矩阵。只运行受支持适配器生成的 argv,shell=false;缺工具/超时/非零均失败。", {
236
+ toolI(server, startupRoot, "run_quality_matrix", "显式执行跨语言质量矩阵。只运行受支持适配器生成的 argv,shell=false;缺工具/超时/非零均失败。", {
213
237
  confirm_execute: z.literal(true).describe("必须显式为 true;该操作会运行目标项目构建/测试命令"),
214
238
  unit: z.string().optional(),
215
239
  kinds: z.array(z.enum(["test", "build", "lint", "typecheck", "coverage", "security"])).optional(),
216
240
  stop_on_failure: z.boolean().optional(),
217
- }, async (args) => {
241
+ }, async (args, ctx) => {
242
+ const root = ctx.root;
218
243
  requireInitialized(root);
219
244
  if (args.confirm_execute !== true)
220
245
  throw new Error("必须显式 confirm_execute=true");
@@ -237,9 +262,10 @@ export function registerGovernanceTools(server, root) {
237
262
  lines.push("命令执行结果与任务完成证据分开判断;未知计数不等于已观察测试通过。");
238
263
  return foldLines(lines, { maxLines: budgetLines(root), hint: "查看 .pm/quality-runs 结构化摘要(不落原始输出)" });
239
264
  });
240
- toolR(server, root, "get_portfolio", "聚合当前项目或全局注册项目的阶段、里程碑、债务、安全、语言、跨仓依赖/版本/cycle;加载失败不能假绿。", {
265
+ toolR(server, startupRoot, "get_portfolio", "聚合当前项目或全局注册项目的阶段、里程碑、债务、安全、语言、跨仓依赖/版本/cycle;加载失败不能假绿。", {
241
266
  current_only: z.boolean().optional(),
242
- }, (args) => {
267
+ }, (args, ctx) => {
268
+ const root = ctx.root;
243
269
  requireInitialized(root);
244
270
  const portfolio = args.current_only
245
271
  ? buildPortfolioReport({ projects: [loadPortfolioProject(root)], projectsRequested: 1 })
@@ -35,7 +35,8 @@ export function computeEntry(root, rel, stat) {
35
35
  return {
36
36
  loc: countLoc(content),
37
37
  skip: needsSkipCount(rel, isTest) ? countSkipLines(content) : 0,
38
- trivial: isTest && looksTrivialTest(content),
38
+ // fixtures 是测试的输入而非测试本身,不参与空测试嫌疑
39
+ trivial: isTest && !/(^|\/)fixtures(\/|$)/.test(rel) && looksTrivialTest(content),
39
40
  oversize: false,
40
41
  contentOk: true,
41
42
  };