@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.
- package/LICENSE +21 -0
- package/README.md +304 -0
- package/dist/acceptance-evaluator.js +330 -0
- package/dist/acceptance-model.js +440 -0
- package/dist/acceptance-report.js +335 -0
- package/dist/acceptance-tools.js +318 -0
- package/dist/audit-tools.js +61 -0
- package/dist/audit.js +364 -0
- package/dist/budget.js +71 -0
- package/dist/dashboard.js +241 -0
- package/dist/governance-audit.js +126 -0
- package/dist/governance-model.js +306 -0
- package/dist/governance-tools.js +240 -0
- package/dist/health.js +58 -0
- package/dist/index-db.js +97 -0
- package/dist/index-store.js +381 -0
- package/dist/index.js +210 -0
- package/dist/init.js +40 -0
- package/dist/knowledge-tools.js +204 -0
- package/dist/language-adapters.js +450 -0
- package/dist/language-dependencies.js +351 -0
- package/dist/license.js +261 -0
- package/dist/paths.js +51 -0
- package/dist/polyglot-ast.js +329 -0
- package/dist/portfolio.js +349 -0
- package/dist/project-fingerprint.js +83 -0
- package/dist/project-tools.js +217 -0
- package/dist/quality-store.js +77 -0
- package/dist/registry.js +38 -0
- package/dist/roadmap.js +96 -0
- package/dist/scan.js +271 -0
- package/dist/search.js +312 -0
- package/dist/security.js +269 -0
- package/dist/semantic-evidence-store.js +133 -0
- package/dist/semantic-evidence.js +100 -0
- package/dist/semantic-graph-algorithms.js +86 -0
- package/dist/semantic-graph.js +390 -0
- package/dist/semantic-parsers.js +311 -0
- package/dist/store.js +185 -0
- package/dist/task-tools.js +198 -0
- package/dist/tool-base.js +32 -0
- package/dist/tools.js +21 -0
- package/dist/types.js +208 -0
- package/dist/typescript-semantic.js +282 -0
- package/package.json +70 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { budgetLines, tool, toolW } from "./tool-base.js";
|
|
4
|
+
import { foldLines } from "./budget.js";
|
|
5
|
+
import { refreshDerived } from "./dashboard.js";
|
|
6
|
+
import { requireInitialized } from "./paths.js";
|
|
7
|
+
import { initGovernance, loadGovernance, saveGovernance, } from "./governance-model.js";
|
|
8
|
+
import { auditGovernance } from "./governance-audit.js";
|
|
9
|
+
import { impactAnalysis } from "./semantic-graph.js";
|
|
10
|
+
import { assessQualityCoverage, createQualityPlan, discoverProjectUnits, runQualityPlan } from "./language-adapters.js";
|
|
11
|
+
import { buildPortfolioFromRegistry, buildPortfolioReport, loadPortfolioProject } from "./portfolio.js";
|
|
12
|
+
import { saveQualityRun } from "./quality-store.js";
|
|
13
|
+
import { SemanticEvidenceDocumentSchema } from "./semantic-evidence.js";
|
|
14
|
+
import { listSemanticEvidence, saveSemanticEvidence } from "./semantic-evidence-store.js";
|
|
15
|
+
import { fingerprintProject } from "./project-fingerprint.js";
|
|
16
|
+
const id = z.string().trim().min(1);
|
|
17
|
+
const texts = z.array(z.string().trim().min(1));
|
|
18
|
+
const moduleInput = {
|
|
19
|
+
id,
|
|
20
|
+
name: z.string().trim().min(1),
|
|
21
|
+
roots: texts.min(1),
|
|
22
|
+
kind: z.string().trim().min(1),
|
|
23
|
+
owners: texts,
|
|
24
|
+
languages: texts,
|
|
25
|
+
public_interfaces: texts.optional(),
|
|
26
|
+
depends_on: texts.optional(),
|
|
27
|
+
allowed_dependencies: texts.optional(),
|
|
28
|
+
denied_dependencies: texts.optional(),
|
|
29
|
+
};
|
|
30
|
+
function replaceById(items, value) {
|
|
31
|
+
const index = items.findIndex((item) => item.id === value.id);
|
|
32
|
+
if (index >= 0)
|
|
33
|
+
items[index] = value;
|
|
34
|
+
else
|
|
35
|
+
items.push(value);
|
|
36
|
+
}
|
|
37
|
+
function compactGovernance(governance) {
|
|
38
|
+
const lines = [
|
|
39
|
+
`治理 schema v${governance.schema_version}`,
|
|
40
|
+
`modules ${governance.modules.length} · interfaces ${governance.interfaces.length} · repositories ${governance.repositories.length}`,
|
|
41
|
+
`policies: ownership=${governance.policies.enforce_ownership} · declared=${governance.policies.enforce_declared_dependencies} · public=${governance.policies.enforce_public_interfaces} · unresolved=${governance.policies.fail_on_unresolved} · coverage>=${governance.policies.minimum_coverage_pct}% · semantic>=${governance.policies.minimum_semantic_assurance} · regex-fallback=${governance.policies.fail_on_semantic_fallback ? "forbidden" : "allowed"} · quality=${governance.policies.required_quality_kinds.join(",")}`,
|
|
42
|
+
];
|
|
43
|
+
for (const module of governance.modules) {
|
|
44
|
+
lines.push(`- ${module.id} [${module.kind}] roots=${module.roots.join(",")} owners=${module.owners.join(",") || "无"} languages=${module.languages.join("+") || "无"} depends=${module.depends_on.join(",") || "无"}`);
|
|
45
|
+
}
|
|
46
|
+
for (const item of governance.interfaces)
|
|
47
|
+
lines.push(`- interface ${item.id} ${item.kind}@${item.version}: ${item.provider} -> ${item.consumers.join(",") || "无消费者"}`);
|
|
48
|
+
for (const repo of governance.repositories)
|
|
49
|
+
lines.push(`- repository ${repo.id}@${repo.version}: ${repo.root} -> ${repo.dependencies.map((dependency) => `${dependency.repository}@${dependency.constraint}`).join(",") || "无依赖"}`);
|
|
50
|
+
return foldLines(lines, { maxLines: 150, hint: "用 upsert_module/upsert_interface/upsert_repository 分项维护" });
|
|
51
|
+
}
|
|
52
|
+
function qualityPlan(root, unit, kinds) {
|
|
53
|
+
const units = discoverProjectUnits(root);
|
|
54
|
+
const coverage = assessQualityCoverage(units);
|
|
55
|
+
const selectedUnits = unit ? units.filter((item) => item.id === unit) : units;
|
|
56
|
+
if (unit && selectedUnits.length === 0)
|
|
57
|
+
throw new Error(`找不到质量单元 ${unit}。现有: ${units.map((item) => item.id).join(", ") || "无"}`);
|
|
58
|
+
let commands = createQualityPlan(selectedUnits);
|
|
59
|
+
if (kinds?.length)
|
|
60
|
+
commands = commands.filter((command) => kinds.includes(command.kind));
|
|
61
|
+
return { commands, coverage };
|
|
62
|
+
}
|
|
63
|
+
function qualityLines(commands) {
|
|
64
|
+
return commands.map((command, index) => `${index + 1}. [${command.kind}] (${command.cwd}) ${command.command} ${command.args.join(" ")}`);
|
|
65
|
+
}
|
|
66
|
+
export function registerGovernanceTools(server, root) {
|
|
67
|
+
tool(server, "init_governance", "为旧项目初始化 .pm/governance.json;新项目 init_project 已自动创建。", {}, () => {
|
|
68
|
+
requireInitialized(root);
|
|
69
|
+
const governance = initGovernance(root, {});
|
|
70
|
+
refreshDerived(root);
|
|
71
|
+
return `✅ 治理模型已初始化(schema v${governance.schema_version})。下一步用 upsert_module 声明模块、owner 与语言。`;
|
|
72
|
+
});
|
|
73
|
+
tool(server, "get_governance", "查看结构化模块、owner、公开接口、仓库依赖与强制策略。", {}, () => {
|
|
74
|
+
requireInitialized(root);
|
|
75
|
+
return compactGovernance(loadGovernance(root));
|
|
76
|
+
});
|
|
77
|
+
toolW(server, root, "upsert_module", "新增或完整替换一个治理模块;root/owner/language/依赖边均经 schema 对账。", moduleInput, (args) => {
|
|
78
|
+
requireInitialized(root);
|
|
79
|
+
const governance = loadGovernance(root);
|
|
80
|
+
const value = {
|
|
81
|
+
...args,
|
|
82
|
+
public_interfaces: args.public_interfaces ?? [],
|
|
83
|
+
depends_on: args.depends_on ?? [],
|
|
84
|
+
allowed_dependencies: args.allowed_dependencies ?? [],
|
|
85
|
+
denied_dependencies: args.denied_dependencies ?? [],
|
|
86
|
+
};
|
|
87
|
+
replaceById(governance.modules, value);
|
|
88
|
+
saveGovernance(root, governance);
|
|
89
|
+
refreshDerived(root);
|
|
90
|
+
return `✅ module ${value.id} 已保存(roots ${value.roots.length},owners ${value.owners.length},languages ${value.languages.length})。`;
|
|
91
|
+
});
|
|
92
|
+
toolW(server, root, "upsert_interface", "新增或替换跨模块公开接口及契约文件、provider/consumers、版本。", {
|
|
93
|
+
id,
|
|
94
|
+
kind: z.string().trim().min(1),
|
|
95
|
+
provider: id,
|
|
96
|
+
consumers: texts,
|
|
97
|
+
contract_files: texts,
|
|
98
|
+
version: z.string().trim().min(1),
|
|
99
|
+
}, (args) => {
|
|
100
|
+
requireInitialized(root);
|
|
101
|
+
const governance = loadGovernance(root);
|
|
102
|
+
if (!governance.modules.some((module) => module.id === args.provider))
|
|
103
|
+
throw new Error(`provider module 不存在: ${args.provider}`);
|
|
104
|
+
for (const module of governance.modules)
|
|
105
|
+
module.public_interfaces = module.public_interfaces.filter((item) => item !== args.id);
|
|
106
|
+
governance.modules.find((module) => module.id === args.provider).public_interfaces.push(args.id);
|
|
107
|
+
replaceById(governance.interfaces, args);
|
|
108
|
+
saveGovernance(root, governance);
|
|
109
|
+
refreshDerived(root);
|
|
110
|
+
return `✅ interface ${args.id}@${args.version} 已保存:${args.provider} -> ${args.consumers.join(",") || "无消费者"}。`;
|
|
111
|
+
});
|
|
112
|
+
toolW(server, root, "upsert_repository", "登记当前组合视图中的仓库、版本及跨仓 semver 约束。", {
|
|
113
|
+
id,
|
|
114
|
+
name: z.string().trim().min(1),
|
|
115
|
+
root: z.string().trim().min(1),
|
|
116
|
+
version: z.string().trim().min(1),
|
|
117
|
+
dependencies: z.array(z.object({ repository: id, constraint: z.string().trim().min(1) })),
|
|
118
|
+
}, (args) => {
|
|
119
|
+
requireInitialized(root);
|
|
120
|
+
const governance = loadGovernance(root);
|
|
121
|
+
replaceById(governance.repositories, args);
|
|
122
|
+
saveGovernance(root, governance);
|
|
123
|
+
refreshDerived(root);
|
|
124
|
+
return `✅ repository ${args.id}@${args.version} 已保存(依赖 ${args.dependencies.length})。`;
|
|
125
|
+
});
|
|
126
|
+
toolW(server, root, "set_governance_policies", "更新治理强制策略;未传字段保持原值。", {
|
|
127
|
+
enforce_ownership: z.boolean().optional(),
|
|
128
|
+
enforce_declared_dependencies: z.boolean().optional(),
|
|
129
|
+
fail_on_unresolved: z.boolean().optional(),
|
|
130
|
+
enforce_public_interfaces: z.boolean().optional(),
|
|
131
|
+
minimum_coverage_pct: z.number().min(0).max(100).optional(),
|
|
132
|
+
minimum_semantic_assurance: z.enum(["heuristic", "ast", "runtime"]).optional(),
|
|
133
|
+
fail_on_semantic_fallback: z.boolean().optional(),
|
|
134
|
+
required_quality_kinds: z.array(z.enum(["test", "build", "lint", "typecheck", "coverage", "security"])).optional(),
|
|
135
|
+
}, (args) => {
|
|
136
|
+
requireInitialized(root);
|
|
137
|
+
const governance = loadGovernance(root);
|
|
138
|
+
governance.policies = { ...governance.policies, ...args };
|
|
139
|
+
saveGovernance(root, governance);
|
|
140
|
+
refreshDerived(root);
|
|
141
|
+
return `✅ 治理策略已更新:coverage>=${governance.policies.minimum_coverage_pct}% · semantic>=${governance.policies.minimum_semantic_assurance} · regex-fallback=${governance.policies.fail_on_semantic_fallback ? "forbidden" : "allowed"}`;
|
|
142
|
+
});
|
|
143
|
+
tool(server, "discover_languages", "递归发现 Node/Python/Go/Rust/JVM/.NET 单元及其受控质量命令,不执行命令。", {}, () => {
|
|
144
|
+
requireInitialized(root);
|
|
145
|
+
const units = discoverProjectUnits(root);
|
|
146
|
+
const coverage = assessQualityCoverage(units);
|
|
147
|
+
const lines = [`发现 ${units.length} 个语言单元;质量命令覆盖 ${coverage.coveragePct}%(${coverage.unitsWithCommands}/${coverage.totalUnits})`];
|
|
148
|
+
for (const unit of units)
|
|
149
|
+
lines.push(`- ${unit.id}: ${unit.languages.join("+") || "未知"} · manifests ${unit.manifest.map((item) => path.basename(item.path)).join(",")} · deps ${unit.dependencies.length} · dep-errors ${unit.dependencyErrors.length} · commands ${unit.qualityCommands.map((item) => item.kind).join(",") || "无"}`);
|
|
150
|
+
if (coverage.withoutCommands.length)
|
|
151
|
+
lines.push(`- 🚩 无质量命令: ${coverage.withoutCommands.join(", ")}`);
|
|
152
|
+
return foldLines(lines, { maxLines: budgetLines(root), hint: "按模块拆分治理 root" });
|
|
153
|
+
});
|
|
154
|
+
tool(server, "audit_governance", "审计跨文件/模块/语言语义覆盖、owner、公开接口、依赖边界、循环、unresolved 与质量矩阵覆盖。", {}, () => {
|
|
155
|
+
requireInitialized(root);
|
|
156
|
+
return auditGovernance(root, budgetLines(root)).report;
|
|
157
|
+
});
|
|
158
|
+
tool(server, "list_semantic_evidence", "列出已登记的语言原生 AST、编译器或运行时语义证据;损坏文档会使整次读取失败。", {}, () => {
|
|
159
|
+
requireInitialized(root);
|
|
160
|
+
return JSON.stringify(listSemanticEvidence(root), null, 2);
|
|
161
|
+
});
|
|
162
|
+
tool(server, "save_semantic_evidence", "保存一份绑定源码 SHA-256 的语言原生 AST/编译器/运行时证据;过期摘要在治理审计中严格失败。", {
|
|
163
|
+
id: z.string().trim().min(1).max(128),
|
|
164
|
+
document: SemanticEvidenceDocumentSchema,
|
|
165
|
+
}, (args) => {
|
|
166
|
+
requireInitialized(root);
|
|
167
|
+
const saved = saveSemanticEvidence(root, args.id, args.document);
|
|
168
|
+
return `✅ semantic evidence ${args.id} 已保存:${saved.file} · ${saved.analyzer.id} · ${saved.analyzer.assurance} · ${saved.status}`;
|
|
169
|
+
});
|
|
170
|
+
tool(server, "impact_analysis", "从变更文件沿 file/module 反向依赖闭包计算受影响文件与模块。", {
|
|
171
|
+
files: z.array(z.string().min(1)).min(1).max(100),
|
|
172
|
+
}, (args) => {
|
|
173
|
+
requireInitialized(root);
|
|
174
|
+
const audit = auditGovernance(root, budgetLines(root));
|
|
175
|
+
const impact = impactAnalysis(audit.graph, args.files);
|
|
176
|
+
return foldLines([
|
|
177
|
+
`变更 ${impact.changedFiles.length} · 受影响文件 ${impact.impactedFiles.length} · 模块 ${impact.impactedModules.length}`,
|
|
178
|
+
`模块: ${impact.impactedModules.join(", ") || "无"}`,
|
|
179
|
+
...impact.dependentFiles.slice(0, 80).map((file) => `- ${file}`),
|
|
180
|
+
...(impact.unknownChangedFiles.length ? [`⚠️ 未进入语义图: ${impact.unknownChangedFiles.join(", ")}`] : []),
|
|
181
|
+
], { maxLines: budgetLines(root), hint: "缩小 files 输入或按模块运行" });
|
|
182
|
+
});
|
|
183
|
+
tool(server, "plan_quality_matrix", "生成跨语言真实质量命令计划;只规划,不启动进程。", {
|
|
184
|
+
unit: z.string().optional(),
|
|
185
|
+
kinds: z.array(z.enum(["test", "build", "lint", "typecheck", "coverage", "security"])).optional(),
|
|
186
|
+
}, (args) => {
|
|
187
|
+
requireInitialized(root);
|
|
188
|
+
const plan = qualityPlan(root, args.unit, args.kinds);
|
|
189
|
+
return foldLines([
|
|
190
|
+
`质量矩阵计划 ${plan.commands.length} 条;unit coverage ${plan.coverage.coveragePct}%(plan-only,未执行)`,
|
|
191
|
+
...qualityLines(plan.commands),
|
|
192
|
+
...(plan.coverage.withoutCommands.length ? [`🚩 无命令单元: ${plan.coverage.withoutCommands.join(", ")}`] : []),
|
|
193
|
+
], { maxLines: budgetLines(root), hint: "用 unit/kinds 过滤;真实执行需 run_quality_matrix confirm_execute=true" });
|
|
194
|
+
});
|
|
195
|
+
tool(server, "run_quality_matrix", "显式执行跨语言质量矩阵。只运行受支持适配器生成的 argv,shell=false;缺工具/超时/非零均失败。", {
|
|
196
|
+
confirm_execute: z.literal(true).describe("必须显式为 true;该操作会运行目标项目构建/测试命令"),
|
|
197
|
+
unit: z.string().optional(),
|
|
198
|
+
kinds: z.array(z.enum(["test", "build", "lint", "typecheck", "coverage", "security"])).optional(),
|
|
199
|
+
stop_on_failure: z.boolean().optional(),
|
|
200
|
+
}, async (args) => {
|
|
201
|
+
requireInitialized(root);
|
|
202
|
+
if (args.confirm_execute !== true)
|
|
203
|
+
throw new Error("必须显式 confirm_execute=true");
|
|
204
|
+
const plan = qualityPlan(root, args.unit, args.kinds);
|
|
205
|
+
if (plan.coverage.withoutCommands.length)
|
|
206
|
+
throw new Error(`质量矩阵不完整,无命令单元: ${plan.coverage.withoutCommands.join(", ")}`);
|
|
207
|
+
if (plan.commands.length === 0)
|
|
208
|
+
throw new Error("质量矩阵为空,拒绝制造假绿");
|
|
209
|
+
const sourceBefore = fingerprintProject(root);
|
|
210
|
+
const executed = await runQualityPlan(plan.commands, { execute: true, stopOnFailure: args.stop_on_failure });
|
|
211
|
+
const sourceAfter = fingerprintProject(root);
|
|
212
|
+
const sourceStable = sourceBefore.sha256 === sourceAfter.sha256;
|
|
213
|
+
const result = { ...executed, ok: executed.ok && sourceStable };
|
|
214
|
+
const file = saveQualityRun(root, result, { source_before: sourceBefore, source_after: sourceAfter });
|
|
215
|
+
const lines = [`${result.ok ? "✅" : "🚩"} 质量矩阵 ${result.ok ? "通过" : "失败"};结果 ${file}`];
|
|
216
|
+
if (!sourceStable)
|
|
217
|
+
lines.push(`- 🚩 源码在执行期间变化:before=${sourceBefore.sha256} after=${sourceAfter.sha256}`);
|
|
218
|
+
for (const item of result.results)
|
|
219
|
+
lines.push(`- [${item.status}] ${item.command.kind} ${item.command.cwd} (${item.durationMs}ms${item.exitCode === null ? "" : `, exit ${item.exitCode}`})`);
|
|
220
|
+
return foldLines(lines, { maxLines: budgetLines(root), hint: "查看 .pm/quality-runs 结构化摘要(不落原始输出)" });
|
|
221
|
+
});
|
|
222
|
+
tool(server, "get_portfolio", "聚合当前项目或全局注册项目的阶段、里程碑、债务、安全、语言、跨仓依赖/版本/cycle;加载失败不能假绿。", {
|
|
223
|
+
current_only: z.boolean().optional(),
|
|
224
|
+
}, (args) => {
|
|
225
|
+
requireInitialized(root);
|
|
226
|
+
const portfolio = args.current_only
|
|
227
|
+
? buildPortfolioReport({ projects: [loadPortfolioProject(root)], projectsRequested: 1 })
|
|
228
|
+
: buildPortfolioFromRegistry();
|
|
229
|
+
const lines = [
|
|
230
|
+
`${portfolio.ok ? "✅" : "🚩"} portfolio projects ${portfolio.coverage.projects_loaded}/${portfolio.coverage.projects_requested} · failures ${portfolio.coverage.projects_failed} · repo edges ${portfolio.coverage.repository_dependencies} · violations ${portfolio.violations.length}`,
|
|
231
|
+
];
|
|
232
|
+
for (const project of portfolio.projects)
|
|
233
|
+
lines.push(`- ${project.name}: ${project.phase || "无阶段"} · milestone active ${project.milestones.active}/${project.milestones.total} · debt ${project.tasks.debt_open} · security ${project.security.open} · languages ${project.languages.join("+") || "无"}`);
|
|
234
|
+
for (const violation of portfolio.violations.slice(0, 20))
|
|
235
|
+
lines.push(`- ${violation.severity === "error" ? "🚩" : "⚠️"} [${violation.code}] ${violation.message}`);
|
|
236
|
+
for (const failure of portfolio.projectFailures.slice(0, 10))
|
|
237
|
+
lines.push(`- 🚩 load ${failure.name}: ${failure.error}`);
|
|
238
|
+
return foldLines(lines, { maxLines: budgetLines(root), hint: "修复 projectFailures/跨仓版本或循环后重跑" });
|
|
239
|
+
});
|
|
240
|
+
}
|
package/dist/health.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** 距今天数 */
|
|
2
|
+
export function ageDays(iso, nowMs = Date.now()) {
|
|
3
|
+
return Math.max(0, Math.floor((nowMs - Date.parse(iso)) / 86_400_000));
|
|
4
|
+
}
|
|
5
|
+
export function churnStats(sessions, threshold = 3, topN = 10) {
|
|
6
|
+
const counts = new Map();
|
|
7
|
+
for (const s of sessions) {
|
|
8
|
+
for (const f of s.files)
|
|
9
|
+
counts.set(f, (counts.get(f) ?? 0) + 1);
|
|
10
|
+
}
|
|
11
|
+
const hotspots = [...counts.entries()]
|
|
12
|
+
.filter(([, c]) => c >= threshold)
|
|
13
|
+
.map(([file, count]) => ({ file, count }))
|
|
14
|
+
.sort((a, b) => b.count - a.count)
|
|
15
|
+
.slice(0, topN);
|
|
16
|
+
return { hotspots, sessionCount: sessions.length };
|
|
17
|
+
}
|
|
18
|
+
export function debtAging(tasks, nowMs = Date.now()) {
|
|
19
|
+
const open = tasks.filter((t) => t.type === "debt" && t.status !== "done" && t.status !== "cancelled");
|
|
20
|
+
let oldestDays = 0;
|
|
21
|
+
let oldestTask = null;
|
|
22
|
+
for (const t of open) {
|
|
23
|
+
const d = ageDays(t.created, nowMs);
|
|
24
|
+
if (d > oldestDays) {
|
|
25
|
+
oldestDays = d;
|
|
26
|
+
oldestTask = `${t.id} ${t.title}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { openDebt: open.length, oldestDays, oldestTask };
|
|
30
|
+
}
|
|
31
|
+
/* -------------------------------- 波及面告警 ------------------------------- */
|
|
32
|
+
export function blastRadiusWarning(sessions, budgets) {
|
|
33
|
+
const last = sessions[sessions.length - 1];
|
|
34
|
+
if (!last)
|
|
35
|
+
return null;
|
|
36
|
+
if (last.files.length > budgets.sessionBlastRadius) {
|
|
37
|
+
return `最近一次会话波及 ${last.files.length} 个文件,超过阈值 ${budgets.sessionBlastRadius}——建议拆小任务或先立重构任务(散弹式修改嫌疑:${last.files.slice(0, 5).join(", ")}…)`;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
export function footprint(sessions, tasks, nowMs = Date.now()) {
|
|
42
|
+
const recent = sessions.slice(-5).reverse();
|
|
43
|
+
const cutoff = nowMs - 30 * 86_400_000;
|
|
44
|
+
const completed30d = tasks.filter((t) => t.completed_at && Date.parse(t.completed_at) >= cutoff).length;
|
|
45
|
+
const avg = sessions.length === 0
|
|
46
|
+
? 0
|
|
47
|
+
: Math.round((sessions.reduce((s, x) => s + x.files.length, 0) / sessions.length) * 10) / 10;
|
|
48
|
+
return {
|
|
49
|
+
recentSessions: recent.map((s) => ({
|
|
50
|
+
date: s.date.slice(0, 10),
|
|
51
|
+
author: s.author || "未知",
|
|
52
|
+
files: s.files.length,
|
|
53
|
+
summary: s.summary.length > 60 ? s.summary.slice(0, 60) + "…" : s.summary,
|
|
54
|
+
})),
|
|
55
|
+
tasksCompleted30d: completed30d,
|
|
56
|
+
avgFilesPerSession: avg,
|
|
57
|
+
};
|
|
58
|
+
}
|
package/dist/index-db.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import { pmPath } from "./paths.js";
|
|
5
|
+
/** Low-level SQLite lifecycle, metadata, row iteration, and aggregate queries. */
|
|
6
|
+
const SCHEMA = `
|
|
7
|
+
CREATE TABLE IF NOT EXISTS files(
|
|
8
|
+
rel TEXT PRIMARY KEY,
|
|
9
|
+
top TEXT NOT NULL,
|
|
10
|
+
root1 TEXT NOT NULL,
|
|
11
|
+
dir TEXT NOT NULL,
|
|
12
|
+
ext TEXT NOT NULL,
|
|
13
|
+
is_test INTEGER NOT NULL,
|
|
14
|
+
is_lock INTEGER NOT NULL,
|
|
15
|
+
mtime REAL NOT NULL,
|
|
16
|
+
size INTEGER NOT NULL,
|
|
17
|
+
loc INTEGER NOT NULL DEFAULT 0,
|
|
18
|
+
skip INTEGER NOT NULL DEFAULT 0,
|
|
19
|
+
trivial INTEGER NOT NULL DEFAULT 0,
|
|
20
|
+
oversize INTEGER NOT NULL DEFAULT 0,
|
|
21
|
+
content_ok INTEGER NOT NULL DEFAULT 0
|
|
22
|
+
);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_files_top ON files(top);
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_files_root1 ON files(root1);
|
|
25
|
+
CREATE TABLE IF NOT EXISTS meta(k TEXT PRIMARY KEY, v TEXT NOT NULL);
|
|
26
|
+
`;
|
|
27
|
+
const dbs = new Map();
|
|
28
|
+
export function getIndex(root) {
|
|
29
|
+
const abs = path.resolve(root);
|
|
30
|
+
let db = dbs.get(abs);
|
|
31
|
+
if (db)
|
|
32
|
+
return db;
|
|
33
|
+
fs.mkdirSync(pmPath(abs), { recursive: true });
|
|
34
|
+
db = new DatabaseSync(pmPath(abs, "index.db"));
|
|
35
|
+
// journal_mode=WAL itself may need an exclusive lock. Configure the wait
|
|
36
|
+
// policy first so two MCP clients starting together do not race and crash.
|
|
37
|
+
db.exec("PRAGMA busy_timeout=8000");
|
|
38
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
39
|
+
db.exec("PRAGMA synchronous=NORMAL");
|
|
40
|
+
db.exec(SCHEMA);
|
|
41
|
+
dbs.set(abs, db);
|
|
42
|
+
return db;
|
|
43
|
+
}
|
|
44
|
+
/** 关闭并释放索引连接(Windows 上打开的 SQLite 文件无法被删除/移动) */
|
|
45
|
+
export function closeIndex(root) {
|
|
46
|
+
const abs = path.resolve(root);
|
|
47
|
+
const db = dbs.get(abs);
|
|
48
|
+
if (db) {
|
|
49
|
+
try {
|
|
50
|
+
db.close();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* ignore */
|
|
54
|
+
}
|
|
55
|
+
dbs.delete(abs);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function getMeta(db, key) {
|
|
59
|
+
const row = db.prepare("SELECT v FROM meta WHERE k=?").get(key);
|
|
60
|
+
return row?.v ?? null;
|
|
61
|
+
}
|
|
62
|
+
export function setMeta(db, key, value) {
|
|
63
|
+
db.prepare("INSERT INTO meta(k,v) VALUES(?,?) ON CONFLICT(k) DO UPDATE SET v=excluded.v").run(key, value);
|
|
64
|
+
}
|
|
65
|
+
// Static SQL variants keep statements reusable and avoid dynamic SQL construction.
|
|
66
|
+
const SQL_ROWS_ALL = "SELECT rel, ext, is_test, oversize FROM files WHERE is_lock=0 ORDER BY rel";
|
|
67
|
+
const SQL_ROWS_NO_OVERSIZE = "SELECT rel, ext, is_test, oversize FROM files WHERE is_lock=0 AND oversize=0 ORDER BY rel";
|
|
68
|
+
/** 流式遍历索引行(不整体物化;filter 缺省=全部非锁文件) */
|
|
69
|
+
export function* iterateFileRows(db, filter = {}) {
|
|
70
|
+
const stmt = db.prepare(filter.excludeOversize ? SQL_ROWS_NO_OVERSIZE : SQL_ROWS_ALL);
|
|
71
|
+
for (const row of stmt.iterate()) {
|
|
72
|
+
if (filter.excludeTest && row.is_test === 1)
|
|
73
|
+
continue;
|
|
74
|
+
yield row;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function aggregates(db) {
|
|
78
|
+
const one = (sql) => db.prepare(sql).get();
|
|
79
|
+
const total = one(`SELECT COUNT(*) c, COALESCE(SUM(CASE WHEN is_lock=0 THEN loc ELSE 0 END),0) loc FROM files`);
|
|
80
|
+
const test = one(`SELECT COUNT(*) c FROM files WHERE is_test=1`);
|
|
81
|
+
const skip = one(`SELECT COALESCE(SUM(skip),0) s FROM files`);
|
|
82
|
+
const contentOk = one(`SELECT COUNT(*) c FROM files WHERE content_ok=1`);
|
|
83
|
+
return {
|
|
84
|
+
totalFiles: total.c,
|
|
85
|
+
totalLoc: total.loc,
|
|
86
|
+
testFiles: test.c,
|
|
87
|
+
skipMarkers: skip.s,
|
|
88
|
+
trivialTests: db.prepare(`SELECT rel FROM files WHERE trivial=1 ORDER BY rel LIMIT 20`).all().map((r) => r.rel),
|
|
89
|
+
oversizeFiles: db.prepare(`SELECT rel FROM files WHERE oversize=1 ORDER BY rel LIMIT 20`).all().map((r) => r.rel),
|
|
90
|
+
largestFiles: db.prepare(`SELECT rel AS path, loc FROM files WHERE is_lock=0 ORDER BY loc DESC, rel LIMIT 10`).all(),
|
|
91
|
+
byExt: db.prepare(`SELECT ext, COUNT(*) files, COALESCE(SUM(loc),0) loc FROM files WHERE is_lock=0 GROUP BY ext ORDER BY loc DESC`).all(),
|
|
92
|
+
topDirs: db.prepare(`SELECT top AS dir, COUNT(*) files, COALESCE(SUM(loc),0) loc FROM files GROUP BY top ORDER BY loc DESC LIMIT 10`).all(),
|
|
93
|
+
rootDirs: db.prepare(`SELECT DISTINCT root1 FROM files`).all().map((r) => r.root1),
|
|
94
|
+
contentOk: contentOk.c,
|
|
95
|
+
indexCoverageBase: contentOk.c,
|
|
96
|
+
};
|
|
97
|
+
}
|