@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,241 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { atomicWrite, loadDebugLog, loadFeatures, loadProject, loadRoadmap, loadSecurity, loadSessions, loadTasks, } from "./store.js";
|
|
4
|
+
import { decisionsDir, pmPath, dashboardFile } from "./paths.js";
|
|
5
|
+
import { mermaidRoadmap, quotaWarnings, renderRoadmap, checkpointSuffix } from "./roadmap.js";
|
|
6
|
+
import { churnStats, debtAging } from "./health.js";
|
|
7
|
+
import { featureVerifyMark } from "./audit.js";
|
|
8
|
+
import { loadGovernance } from "./governance-model.js";
|
|
9
|
+
import { touchRegistry } from "./registry.js";
|
|
10
|
+
/** 生成 PROJECT.md 全文 */
|
|
11
|
+
export function buildDashboard(root) {
|
|
12
|
+
const project = loadProject(root);
|
|
13
|
+
const { milestones } = loadRoadmap(root);
|
|
14
|
+
const { tasks } = loadTasks(root);
|
|
15
|
+
const { features } = loadFeatures(root);
|
|
16
|
+
const { sessions } = loadSessions(root);
|
|
17
|
+
const { entries: debugEntries } = loadDebugLog(root);
|
|
18
|
+
const security = loadSecurity(root);
|
|
19
|
+
let governance = null;
|
|
20
|
+
try {
|
|
21
|
+
governance = loadGovernance(root);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// 旧项目迁移前在仪表盘显式显示缺失,不让派生刷新整体失败。
|
|
25
|
+
}
|
|
26
|
+
let latestQuality = null;
|
|
27
|
+
try {
|
|
28
|
+
const dir = pmPath(root, "quality-runs");
|
|
29
|
+
const latest = fs.readdirSync(dir).filter((name) => name.endsWith(".json")).sort().at(-1);
|
|
30
|
+
if (latest)
|
|
31
|
+
latestQuality = JSON.parse(fs.readFileSync(path.join(dir, latest), "utf8"));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// 尚未运行真实质量矩阵。
|
|
35
|
+
}
|
|
36
|
+
let latestAcceptance = null;
|
|
37
|
+
try {
|
|
38
|
+
const dir = pmPath(root, "acceptance", "reports");
|
|
39
|
+
const latest = fs.readdirSync(dir)
|
|
40
|
+
.filter((name) => name.endsWith(".json") && !name.endsWith(".sha256.json"))
|
|
41
|
+
.flatMap((name) => {
|
|
42
|
+
try {
|
|
43
|
+
const report = JSON.parse(fs.readFileSync(path.join(dir, name), "utf8"));
|
|
44
|
+
const timestamp = report ? Date.parse(report.report_generated_at) : Number.NaN;
|
|
45
|
+
return Number.isFinite(timestamp) ? [{ name, timestamp, report }] : [];
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
// Git checkout commonly gives every tracked report the same mtime, so
|
|
52
|
+
// report content—not filesystem metadata—must determine recency.
|
|
53
|
+
.sort((a, b) => b.timestamp - a.timestamp || b.name.localeCompare(a.name))[0];
|
|
54
|
+
if (latest)
|
|
55
|
+
latestAcceptance = latest.report;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// 尚未执行标准化验收。
|
|
59
|
+
}
|
|
60
|
+
const L = [];
|
|
61
|
+
L.push(`# ${project.name} — 项目仪表盘`);
|
|
62
|
+
L.push("");
|
|
63
|
+
L.push(`> ⚠️ 本文件由 pm-mcp 自动生成(勿手改)。状态账本写入后自动刷新;手动刷新用 regenerate_dashboard。`);
|
|
64
|
+
L.push(`> 生成时间: ${new Date().toISOString()}`);
|
|
65
|
+
if (project.description)
|
|
66
|
+
L.push(`> ${project.description}`);
|
|
67
|
+
L.push("");
|
|
68
|
+
/* 路线图置顶 */
|
|
69
|
+
L.push("## 🗺️ 路线图");
|
|
70
|
+
L.push("");
|
|
71
|
+
const mm = mermaidRoadmap(milestones);
|
|
72
|
+
if (mm.length > 0)
|
|
73
|
+
L.push(...mm, "");
|
|
74
|
+
L.push(...renderRoadmap(milestones, tasks, 1));
|
|
75
|
+
const warns = quotaWarnings(project, milestones, tasks);
|
|
76
|
+
for (const w of warns) {
|
|
77
|
+
L.push(`- ⚠️ 重构被挤出: ${w.milestone} 重构类占比 ${w.refactorPct}% < 配额 ${w.quotaPct}%`);
|
|
78
|
+
}
|
|
79
|
+
L.push("");
|
|
80
|
+
/* 当前焦点(断点恢复点) */
|
|
81
|
+
L.push("## 🎯 当前焦点");
|
|
82
|
+
const active = tasks.filter((t) => t.status === "in_progress" || t.status === "blocked");
|
|
83
|
+
if (active.length === 0) {
|
|
84
|
+
L.push("- (无进行中任务。从 backlog 挑一个开始,或 add_task 创建。)");
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
for (const t of active.slice(0, 10)) {
|
|
88
|
+
const next = t.checkpoint ? ` → 下一步: ${t.checkpoint.next_step}${checkpointSuffix(t.checkpoint)}` : "";
|
|
89
|
+
const steps = t.steps.length > 0 ? `(步骤 ${t.steps.filter((s) => s.done).length}/${t.steps.length})` : "";
|
|
90
|
+
L.push(`- ${t.status === "blocked" ? "🚫" : "🔄"} ${t.id} ${t.title}${steps}${next}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (project.phase)
|
|
94
|
+
L.push(`- 当前阶段: ${project.phase}`);
|
|
95
|
+
L.push("");
|
|
96
|
+
/* 健康摘要 */
|
|
97
|
+
L.push("## 🩺 健康摘要");
|
|
98
|
+
const aging = debtAging(tasks);
|
|
99
|
+
const openFindings = security.findings.filter((f) => f.status === "open");
|
|
100
|
+
const highFindings = openFindings.filter((f) => f.severity === "high");
|
|
101
|
+
const churn = churnStats(sessions);
|
|
102
|
+
const implemented = features.filter((f) => f.status === "implemented");
|
|
103
|
+
const drifted = implemented.filter((f) => f.entry_files.some((p) => !fs.existsSync(path.join(root, p))));
|
|
104
|
+
L.push(`| 账本 | 状态 |`);
|
|
105
|
+
L.push(`|---|---|`);
|
|
106
|
+
L.push(`| 漂移(防幻觉) | ${drifted.length === 0 ? "✅ 无" : `⚠️ ${drifted.length} 个功能入口文件缺失`} |`);
|
|
107
|
+
L.push(`| 债务(反挤出) | ${aging.openDebt === 0 ? "✅ 无未清债务" : `⚠️ ${aging.openDebt} 条,最老 ${aging.oldestDays} 天`} |`);
|
|
108
|
+
L.push(`| churn(变更率) | ${churn.hotspots.length === 0 ? "✅ 无热点" : `⚠️ 热点 ${churn.hotspots.slice(0, 3).map((h) => `${h.file}(${h.count})`).join(", ")}`} |`);
|
|
109
|
+
L.push(`| 安全 | ${openFindings.length === 0 ? "✅ 无未处理发现" : `⚠️ ${openFindings.length} 个未处理(高危 ${highFindings.length})`} |`);
|
|
110
|
+
L.push(`| 调试知识 | ${debugEntries.length} 条记录 |`);
|
|
111
|
+
L.push(`| 测试背书 | ${implemented.length === 0 ? "—" : `${implemented.filter((f) => f.test_files.length > 0).length}/${implemented.length} 个功能带测试`} |`);
|
|
112
|
+
L.push(`| 语义治理 | ${governance ? `${governance.modules.length} 模块 / ${governance.interfaces.length} 接口 / ${governance.repositories.length} 仓库` : "⚠️ 未初始化"} |`);
|
|
113
|
+
L.push(`| 质量矩阵 | ${latestQuality ? `${latestQuality.ok ? "✅" : "🚩"} ${latestQuality.run_at.slice(0, 16).replace("T", " ")}(${latestQuality.results.filter((item) => item.status === "passed").length}/${latestQuality.results.length})` : "⚠️ 尚无真实执行记录"} |`);
|
|
114
|
+
L.push(`| 标准化验收 | ${latestAcceptance ? `${latestAcceptance.verdict === "accepted" ? "✅" : "🚩"} ${latestAcceptance.report_generated_at.slice(0, 16).replace("T", " ")}(需求 ${latestAcceptance.summary.requirements_passed}/${latestAcceptance.summary.requirements_total},errors ${latestAcceptance.summary.errors})` : "⚠️ 尚无正式评价报告"} |`);
|
|
115
|
+
L.push("");
|
|
116
|
+
/* 跨模块/语言治理(这里只读账本,不在每次写操作时重跑全仓语义图) */
|
|
117
|
+
L.push("## 🧭 模块与语言治理");
|
|
118
|
+
if (!governance) {
|
|
119
|
+
L.push("- ⚠️ governance.json 缺失;用 init_governance/upsert_module 完成迁移。");
|
|
120
|
+
}
|
|
121
|
+
else if (governance.modules.length === 0) {
|
|
122
|
+
L.push("- ⚠️ 尚未登记结构化模块;文件级账本可用,但依赖/owner/接口边界尚未启用。");
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
for (const module of governance.modules.slice(0, 12)) {
|
|
126
|
+
L.push(`- ${module.id} [${module.kind}] ${module.languages.join("+") || "未知语言"} · owner ${module.owners.join(",") || "未声明"} · roots ${module.roots.join(",")}`);
|
|
127
|
+
}
|
|
128
|
+
L.push(`- 策略: ownership=${governance.policies.enforce_ownership} · declared-deps=${governance.policies.enforce_declared_dependencies} · public-interfaces=${governance.policies.enforce_public_interfaces} · unresolved=${governance.policies.fail_on_unresolved} · source-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(",")}`);
|
|
129
|
+
L.push("- 实时语义结果:pm://architecture / audit_governance;跨仓:pm://portfolio。");
|
|
130
|
+
}
|
|
131
|
+
L.push("");
|
|
132
|
+
/* 任务概览 */
|
|
133
|
+
L.push("## 📋 任务");
|
|
134
|
+
const byStatus = new Map();
|
|
135
|
+
for (const t of tasks)
|
|
136
|
+
byStatus.set(t.status, (byStatus.get(t.status) ?? 0) + 1);
|
|
137
|
+
L.push(`- 总览: ${[...byStatus.entries()].map(([k, v]) => `${k} ${v}`).join(" · ") || "(无任务)"}`);
|
|
138
|
+
const todo = tasks
|
|
139
|
+
.filter((t) => t.status === "todo" || t.status === "in_progress")
|
|
140
|
+
.slice(0, 8);
|
|
141
|
+
for (const t of todo)
|
|
142
|
+
L.push(`- [${t.status}] ${t.id} ${t.title} (${t.type}${t.milestone ? `, ${t.milestone}` : ""})`);
|
|
143
|
+
L.push("");
|
|
144
|
+
/* 功能清单(按模块分组,带验证状态) */
|
|
145
|
+
L.push("## 🧩 功能清单");
|
|
146
|
+
if (features.length === 0) {
|
|
147
|
+
L.push("- (暂无登记。功能落地后用 register_feature 登记,回答「这个项目有哪些功能」。)");
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const groups = new Map();
|
|
151
|
+
for (const f of features) {
|
|
152
|
+
const key = f.module || "未分组";
|
|
153
|
+
const arr = groups.get(key) ?? [];
|
|
154
|
+
arr.push(f);
|
|
155
|
+
groups.set(key, arr);
|
|
156
|
+
}
|
|
157
|
+
for (const [mod, list] of groups) {
|
|
158
|
+
L.push(`### ${mod}`);
|
|
159
|
+
for (const f of list) {
|
|
160
|
+
const mark = f.status !== "implemented" ? "○" : featureVerifyMark(root, f.entry_files, f.test_files);
|
|
161
|
+
L.push(`- ${mark} ${f.id} ${f.name} — ${f.description.slice(0, 80)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
L.push("");
|
|
166
|
+
/* 架构决策 */
|
|
167
|
+
L.push("## 🏛️ 架构决策(最近)");
|
|
168
|
+
const adrDir = decisionsDir(root);
|
|
169
|
+
if (fs.existsSync(adrDir)) {
|
|
170
|
+
const names = fs.readdirSync(adrDir).filter((n) => n.endsWith(".md")).sort().reverse().slice(0, 5);
|
|
171
|
+
if (names.length === 0)
|
|
172
|
+
L.push("- (暂无)");
|
|
173
|
+
for (const n of names)
|
|
174
|
+
L.push(`- [${n.replace(/\.md$/, "")}](${`.pm/decisions/${n}`})`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
L.push("- (暂无)");
|
|
178
|
+
}
|
|
179
|
+
L.push("");
|
|
180
|
+
/* 最近会话 */
|
|
181
|
+
L.push("## 📜 最近会话");
|
|
182
|
+
if (sessions.length === 0) {
|
|
183
|
+
L.push("- (暂无。收工时用 log_session 记录。)");
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
for (const s of sessions.slice(-5).reverse()) {
|
|
187
|
+
L.push(`- ${s.date.slice(0, 10)} [${s.author || "?"}] ${s.summary}`);
|
|
188
|
+
if (s.files.length > 0)
|
|
189
|
+
L.push(` - 改动: ${s.files.slice(0, 8).join(", ")}${s.files.length > 8 ? ` 等 ${s.files.length} 个` : ""}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
L.push("");
|
|
193
|
+
L.push("---");
|
|
194
|
+
L.push(`stack: ${project.stack.join(", ") || "—"} · modules: ${project.modules.join(", ") || "—"} · exposure: ${project.exposure} · license: ${project.license || "未声明"}`);
|
|
195
|
+
return L.join("\n") + "\n";
|
|
196
|
+
}
|
|
197
|
+
/** 从 sessions.json 生成 changelog.md */
|
|
198
|
+
export function buildChangelog(root) {
|
|
199
|
+
const { sessions } = loadSessions(root);
|
|
200
|
+
const L = ["# 变更日志(自动生成,来自 sessions.json)", ""];
|
|
201
|
+
for (const s of [...sessions].reverse()) {
|
|
202
|
+
L.push(`## ${s.date.slice(0, 10)} — ${s.author || "未知"}`);
|
|
203
|
+
L.push("");
|
|
204
|
+
L.push(s.summary);
|
|
205
|
+
if (s.files.length > 0) {
|
|
206
|
+
L.push("");
|
|
207
|
+
L.push(`改动文件(${s.files.length}): ${s.files.join(", ")}`);
|
|
208
|
+
}
|
|
209
|
+
if (s.next_steps.length > 0) {
|
|
210
|
+
L.push("");
|
|
211
|
+
L.push(`下一步: ${s.next_steps.join(";")}`);
|
|
212
|
+
}
|
|
213
|
+
L.push("");
|
|
214
|
+
}
|
|
215
|
+
if (sessions.length === 0)
|
|
216
|
+
L.push("(暂无会话记录。)");
|
|
217
|
+
return L.join("\n") + "\n";
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 派生刷新:每次写操作后调用。重写 PROJECT.md 与 changelog.md,并 touch 全局注册表。
|
|
221
|
+
* 任何派生失败只写 stderr,绝不回滚主操作。
|
|
222
|
+
*/
|
|
223
|
+
export function refreshDerived(root) {
|
|
224
|
+
try {
|
|
225
|
+
let name = "";
|
|
226
|
+
try {
|
|
227
|
+
name = loadProject(root).name;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// 原子写:多进程并发刷新仪表盘时不会产生半截文件
|
|
233
|
+
atomicWrite(dashboardFile(root), buildDashboard(root));
|
|
234
|
+
atomicWrite(pmPath(root, "changelog.md"), buildChangelog(root));
|
|
235
|
+
if (name)
|
|
236
|
+
touchRegistry(root, name);
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
console.error(`[pm-mcp] 派生刷新失败(不影响主操作): ${e.message}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { foldLines } from "./budget.js";
|
|
4
|
+
import { loadGovernance } from "./governance-model.js";
|
|
5
|
+
import { assessQualityCoverage, createQualityPlan, discoverProjectUnits } from "./language-adapters.js";
|
|
6
|
+
import { buildSemanticGraph, moduleForFile } from "./semantic-graph.js";
|
|
7
|
+
import { loadAllSemanticEvidence } from "./semantic-evidence-store.js";
|
|
8
|
+
function languageIssues(governance, graph) {
|
|
9
|
+
const issues = [];
|
|
10
|
+
for (const module of governance.modules) {
|
|
11
|
+
const matching = graph.files.filter((file) => file.module === module.id && file.status === "parsed");
|
|
12
|
+
const discovered = new Set(matching.map((file) => file.language).filter((language) => language !== "protobuf" && language !== "graphql" && language !== "openapi"));
|
|
13
|
+
if (module.languages.length === 0) {
|
|
14
|
+
issues.push({ severity: "error", code: "module-language-missing", message: `模块 ${module.id} 未声明语言`, evidence: module.roots });
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const missing = [...discovered].filter((language) => !module.languages.includes(language));
|
|
18
|
+
if (missing.length > 0) {
|
|
19
|
+
issues.push({
|
|
20
|
+
severity: "error",
|
|
21
|
+
code: "module-language-drift",
|
|
22
|
+
message: `模块 ${module.id} 发现未声明语言: ${missing.join(", ")}`,
|
|
23
|
+
evidence: matching.slice(0, 10).map((file) => `${file.path} => ${file.language}`),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return issues;
|
|
28
|
+
}
|
|
29
|
+
function declaredArtifactIssues(root, governance) {
|
|
30
|
+
const issues = [];
|
|
31
|
+
for (const module of governance.modules) {
|
|
32
|
+
for (const declaredRoot of module.roots) {
|
|
33
|
+
const absolute = path.resolve(root, declaredRoot);
|
|
34
|
+
if (!fs.existsSync(absolute)) {
|
|
35
|
+
issues.push({ severity: "error", code: "module-root-missing", message: `模块 ${module.id} 的 root 不存在: ${declaredRoot}`, evidence: [declaredRoot] });
|
|
36
|
+
}
|
|
37
|
+
else if (!fs.statSync(absolute).isDirectory()) {
|
|
38
|
+
issues.push({ severity: "error", code: "module-root-not-directory", message: `模块 ${module.id} 的 root 不是目录: ${declaredRoot}`, evidence: [declaredRoot] });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const contract of governance.interfaces) {
|
|
43
|
+
for (const file of contract.contract_files) {
|
|
44
|
+
const absolute = path.resolve(root, file);
|
|
45
|
+
if (!fs.existsSync(absolute)) {
|
|
46
|
+
issues.push({ severity: "error", code: "contract-file-missing", message: `接口 ${contract.id} 的契约文件不存在: ${file}`, evidence: [file] });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const owner = moduleForFile(file, governance.modules);
|
|
50
|
+
if (owner !== contract.provider) {
|
|
51
|
+
issues.push({ severity: "error", code: "contract-provider-mismatch", message: `接口 ${contract.id} 契约文件归属 ${owner ?? "未归属"},不是 provider ${contract.provider}`, evidence: [file] });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return issues;
|
|
56
|
+
}
|
|
57
|
+
function graphIssues(graph) {
|
|
58
|
+
const issues = graph.violations.map((violation) => ({
|
|
59
|
+
severity: "error",
|
|
60
|
+
code: violation.type,
|
|
61
|
+
message: `${violation.type}: ${violation.from} -> ${violation.to}`,
|
|
62
|
+
evidence: violation.evidence,
|
|
63
|
+
}));
|
|
64
|
+
for (const cycle of graph.cycles) {
|
|
65
|
+
issues.push({ severity: "error", code: "module-cycle", message: `模块依赖循环: ${cycle.join(" -> ")} -> ${cycle[0]}`, evidence: cycle });
|
|
66
|
+
}
|
|
67
|
+
return issues;
|
|
68
|
+
}
|
|
69
|
+
function render(governance, graph, units, qualityCoverage, issues, maxLines) {
|
|
70
|
+
const errors = issues.filter((issue) => issue.severity === "error");
|
|
71
|
+
const warnings = issues.filter((issue) => issue.severity === "warning");
|
|
72
|
+
const lines = [
|
|
73
|
+
"## 跨文件/模块/语言治理审计",
|
|
74
|
+
`${errors.length === 0 ? "✅" : "🚩"} modules=${governance.modules.length} · interfaces=${governance.interfaces.length} · repositories=${governance.repositories.length} · errors=${errors.length} · warnings=${warnings.length}`,
|
|
75
|
+
`语义覆盖: source ${graph.coverage.sourceCoveragePct}%(${graph.coverage.parsedFiles}/${graph.coverage.sourceCandidateFiles})· assurance ${graph.coverage.semanticAssurancePct}% · AST ${graph.coverage.astFiles} · runtime ${graph.coverage.runtimeFiles} · heuristic ${graph.coverage.heuristicFiles} · internal resolution ${graph.coverage.resolutionPct}% · unresolved ${graph.coverage.unresolvedInternal}`,
|
|
76
|
+
`关系: files ${graph.files.length} · file edges ${graph.fileEdges.length} · module edges ${graph.moduleEdges.length} · contracts ${graph.contracts.length} · cycles ${graph.cycles.length}`,
|
|
77
|
+
`质量矩阵: units ${qualityCoverage.totalUnits} · covered ${qualityCoverage.unitsWithCommands} · coverage ${qualityCoverage.coveragePct}% · commands ${qualityCoverage.commandKinds.join(", ") || "无"}`,
|
|
78
|
+
`manifest 依赖: ${units.reduce((sum, unit) => sum + unit.dependencies.length, 0)} · parse errors ${units.reduce((sum, unit) => sum + unit.dependencyErrors.length, 0)}`,
|
|
79
|
+
`语言: ${[...new Set(units.flatMap((unit) => unit.languages))].sort().join(", ") || "未发现"}`,
|
|
80
|
+
`> 语义引擎 hybrid-semantic-v2;最低保证 ${governance.policies.minimum_semantic_assurance},正则 fallback ${governance.policies.fail_on_semantic_fallback ? "禁止" : "允许"}。AST 不能证明动态执行;反射、生成代码和数据驱动分发须补 hash-bound runtime evidence。`,
|
|
81
|
+
];
|
|
82
|
+
if (qualityCoverage.withoutCommands.length > 0) {
|
|
83
|
+
lines.push(`- 🚩 有 manifest 但没有质量命令的单元: ${qualityCoverage.withoutCommands.join(", ")}`);
|
|
84
|
+
}
|
|
85
|
+
for (const issue of issues.slice(0, 25)) {
|
|
86
|
+
lines.push(`- ${issue.severity === "error" ? "🚩" : "⚠️"} [${issue.code}] ${issue.message}${issue.evidence.length ? ` — ${issue.evidence.slice(0, 3).join(", ")}` : ""}`);
|
|
87
|
+
}
|
|
88
|
+
if (issues.length > 25)
|
|
89
|
+
lines.push(`…另有 ${issues.length - 25} 个问题未展开`);
|
|
90
|
+
if (graph.coverage.unknownFiles.length > 0)
|
|
91
|
+
lines.push(`- ⚠️ 未识别文件样例: ${graph.coverage.unknownFiles.slice(0, 8).join(", ")}`);
|
|
92
|
+
if (graph.coverage.skippedFiles.length > 0)
|
|
93
|
+
lines.push(`- 🚩 未读取/超限文件样例: ${graph.coverage.skippedFiles.slice(0, 8).join(", ")}`);
|
|
94
|
+
return foldLines(lines, { maxLines, hint: "用 impact_analysis 或治理过滤参数缩小范围" });
|
|
95
|
+
}
|
|
96
|
+
export function auditGovernance(root, maxLines = 150) {
|
|
97
|
+
const absolute = path.resolve(root);
|
|
98
|
+
const governance = loadGovernance(absolute);
|
|
99
|
+
const graph = buildSemanticGraph(absolute, governance, { semanticEvidence: loadAllSemanticEvidence(absolute) });
|
|
100
|
+
const units = discoverProjectUnits(absolute);
|
|
101
|
+
const qualityPlan = createQualityPlan(units);
|
|
102
|
+
const qualityCoverage = assessQualityCoverage(units);
|
|
103
|
+
const issues = [
|
|
104
|
+
...declaredArtifactIssues(absolute, governance),
|
|
105
|
+
...languageIssues(governance, graph),
|
|
106
|
+
...graphIssues(graph),
|
|
107
|
+
];
|
|
108
|
+
if (units.length === 0) {
|
|
109
|
+
issues.push({ severity: "error", code: "quality-units-missing", message: "未发现任何受支持语言 manifest,无法建立质量矩阵", evidence: [] });
|
|
110
|
+
}
|
|
111
|
+
if (qualityCoverage.withoutCommands.length > 0) {
|
|
112
|
+
issues.push({ severity: "error", code: "quality-command-missing", message: `${qualityCoverage.withoutCommands.length} 个单元没有质量命令`, evidence: qualityCoverage.withoutCommands });
|
|
113
|
+
}
|
|
114
|
+
for (const unit of units) {
|
|
115
|
+
for (const error of unit.dependencyErrors) {
|
|
116
|
+
issues.push({ severity: "error", code: "dependency-parse-error", message: `依赖清单解析失败 ${unit.id}: ${error.message}`, evidence: [`${error.sourceManifest}${error.line ? `:${error.line}` : ""} (${error.parser})`] });
|
|
117
|
+
}
|
|
118
|
+
const present = new Set(unit.qualityCommands.map((command) => command.kind));
|
|
119
|
+
const missing = governance.policies.required_quality_kinds.filter((kind) => !present.has(kind));
|
|
120
|
+
if (missing.length > 0) {
|
|
121
|
+
issues.push({ severity: "error", code: "quality-kind-missing", message: `质量单元 ${unit.id} 缺少必需门禁: ${missing.join(", ")}`, evidence: unit.manifest.map((item) => item.path) });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const ok = !issues.some((issue) => issue.severity === "error");
|
|
125
|
+
return { ok, governance, graph, units, qualityPlan, qualityCoverage, issues, report: render(governance, graph, units, qualityCoverage, issues, maxLines) };
|
|
126
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { pmPath } from "./paths.js";
|
|
5
|
+
import { loadJson, saveJson, withLedgerLock } from "./store.js";
|
|
6
|
+
export const GOVERNANCE_SCHEMA_VERSION = 1;
|
|
7
|
+
export const GOVERNANCE_FILE = "governance.json";
|
|
8
|
+
const IdSchema = z.string().trim().min(1);
|
|
9
|
+
const TextSchema = z.string().trim().min(1);
|
|
10
|
+
/**
|
|
11
|
+
* Governance paths are persisted with POSIX separators so the ledger has the
|
|
12
|
+
* same identity on Windows and Unix. Resolution against a checkout is left to
|
|
13
|
+
* callers because repository roots may intentionally be absolute or external.
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeGovernancePath(value) {
|
|
16
|
+
const slashed = value.trim().replace(/\\/g, "/").replace(/\/{2,}/g, "/");
|
|
17
|
+
const normalized = path.posix.normalize(slashed);
|
|
18
|
+
return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
|
|
19
|
+
}
|
|
20
|
+
const GovernancePathSchema = z
|
|
21
|
+
.string()
|
|
22
|
+
.trim()
|
|
23
|
+
.min(1)
|
|
24
|
+
.transform(normalizeGovernancePath)
|
|
25
|
+
.pipe(z.string().min(1));
|
|
26
|
+
const IdListSchema = z.array(IdSchema).default([]);
|
|
27
|
+
const TextListSchema = z.array(TextSchema).default([]);
|
|
28
|
+
const PathListSchema = z.array(GovernancePathSchema).default([]);
|
|
29
|
+
const ModuleRootsSchema = z.array(GovernancePathSchema).min(1, "模块至少需要一个 root");
|
|
30
|
+
export const ModuleSpecSchema = z.object({
|
|
31
|
+
id: IdSchema,
|
|
32
|
+
name: TextSchema,
|
|
33
|
+
roots: ModuleRootsSchema,
|
|
34
|
+
kind: TextSchema,
|
|
35
|
+
owners: TextListSchema,
|
|
36
|
+
languages: TextListSchema,
|
|
37
|
+
public_interfaces: IdListSchema,
|
|
38
|
+
depends_on: IdListSchema,
|
|
39
|
+
allowed_dependencies: IdListSchema,
|
|
40
|
+
denied_dependencies: IdListSchema,
|
|
41
|
+
});
|
|
42
|
+
export const InterfaceSpecSchema = z.object({
|
|
43
|
+
id: IdSchema,
|
|
44
|
+
kind: TextSchema,
|
|
45
|
+
provider: IdSchema,
|
|
46
|
+
consumers: IdListSchema,
|
|
47
|
+
contract_files: PathListSchema,
|
|
48
|
+
version: TextSchema,
|
|
49
|
+
});
|
|
50
|
+
export const RepositoryDependencySchema = z.object({
|
|
51
|
+
repository: IdSchema,
|
|
52
|
+
/** 支持 *、精确 semver、^、~、>=、<=、>、<;其他形式由组合审计标为未知。 */
|
|
53
|
+
constraint: TextSchema.default("*"),
|
|
54
|
+
});
|
|
55
|
+
export const RepositorySpecSchema = z.object({
|
|
56
|
+
id: IdSchema,
|
|
57
|
+
name: TextSchema,
|
|
58
|
+
root: GovernancePathSchema,
|
|
59
|
+
version: TextSchema,
|
|
60
|
+
dependencies: z.array(RepositoryDependencySchema).default([]),
|
|
61
|
+
});
|
|
62
|
+
export const GovernancePoliciesSchema = z.object({
|
|
63
|
+
enforce_ownership: z.boolean().default(true),
|
|
64
|
+
enforce_declared_dependencies: z.boolean().default(true),
|
|
65
|
+
enforce_public_interfaces: z.boolean().default(true),
|
|
66
|
+
fail_on_unresolved: z.boolean().default(true),
|
|
67
|
+
minimum_coverage_pct: z.number().min(0).max(100).default(80),
|
|
68
|
+
minimum_semantic_assurance: z.enum(["heuristic", "ast", "runtime"]).default("heuristic"),
|
|
69
|
+
fail_on_semantic_fallback: z.boolean().default(false),
|
|
70
|
+
required_quality_kinds: z.array(z.enum(["test", "build", "lint", "typecheck", "coverage", "security"])).default(["test", "build"]),
|
|
71
|
+
});
|
|
72
|
+
function addDuplicateIssues(values, collection, ctx) {
|
|
73
|
+
const first = new Map();
|
|
74
|
+
values.forEach((value, index) => {
|
|
75
|
+
const previous = first.get(value);
|
|
76
|
+
if (previous === undefined) {
|
|
77
|
+
first.set(value, index);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
ctx.addIssue({
|
|
81
|
+
code: z.ZodIssueCode.custom,
|
|
82
|
+
path: [collection, index, "id"],
|
|
83
|
+
message: `重复 ID "${value}"(首次出现在 ${collection}.${previous}.id)`,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function addDuplicateListIssues(values, pathPrefix, label, ctx) {
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
values.forEach((value, index) => {
|
|
90
|
+
if (!seen.has(value)) {
|
|
91
|
+
seen.add(value);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
ctx.addIssue({
|
|
95
|
+
code: z.ZodIssueCode.custom,
|
|
96
|
+
path: [...pathPrefix, index],
|
|
97
|
+
message: `${label}包含重复引用 "${value}"`,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export const GovernanceSchema = z
|
|
102
|
+
.object({
|
|
103
|
+
schema_version: z.literal(GOVERNANCE_SCHEMA_VERSION).default(GOVERNANCE_SCHEMA_VERSION),
|
|
104
|
+
modules: z.array(ModuleSpecSchema).default([]),
|
|
105
|
+
interfaces: z.array(InterfaceSpecSchema).default([]),
|
|
106
|
+
repositories: z.array(RepositorySpecSchema).default([]),
|
|
107
|
+
policies: GovernancePoliciesSchema.default({
|
|
108
|
+
enforce_ownership: true,
|
|
109
|
+
enforce_declared_dependencies: true,
|
|
110
|
+
enforce_public_interfaces: true,
|
|
111
|
+
fail_on_unresolved: true,
|
|
112
|
+
minimum_coverage_pct: 80,
|
|
113
|
+
minimum_semantic_assurance: "heuristic",
|
|
114
|
+
fail_on_semantic_fallback: false,
|
|
115
|
+
required_quality_kinds: ["test", "build"],
|
|
116
|
+
}),
|
|
117
|
+
})
|
|
118
|
+
.superRefine((governance, ctx) => {
|
|
119
|
+
addDuplicateIssues(governance.modules.map((module) => module.id), "modules", ctx);
|
|
120
|
+
addDuplicateIssues(governance.interfaces.map((item) => item.id), "interfaces", ctx);
|
|
121
|
+
addDuplicateIssues(governance.repositories.map((repository) => repository.id), "repositories", ctx);
|
|
122
|
+
const moduleIds = new Set(governance.modules.map((module) => module.id));
|
|
123
|
+
const interfaceById = new Map(governance.interfaces.map((item) => [item.id, item]));
|
|
124
|
+
const rootOwner = new Map();
|
|
125
|
+
const repositoryRootOwner = new Map();
|
|
126
|
+
governance.modules.forEach((module, moduleIndex) => {
|
|
127
|
+
addDuplicateListIssues(module.roots, ["modules", moduleIndex, "roots"], "roots", ctx);
|
|
128
|
+
addDuplicateListIssues(module.owners, ["modules", moduleIndex, "owners"], "owners", ctx);
|
|
129
|
+
addDuplicateListIssues(module.languages, ["modules", moduleIndex, "languages"], "languages", ctx);
|
|
130
|
+
addDuplicateListIssues(module.public_interfaces, ["modules", moduleIndex, "public_interfaces"], "public_interfaces", ctx);
|
|
131
|
+
addDuplicateListIssues(module.depends_on, ["modules", moduleIndex, "depends_on"], "depends_on", ctx);
|
|
132
|
+
addDuplicateListIssues(module.allowed_dependencies, ["modules", moduleIndex, "allowed_dependencies"], "allowed_dependencies", ctx);
|
|
133
|
+
addDuplicateListIssues(module.denied_dependencies, ["modules", moduleIndex, "denied_dependencies"], "denied_dependencies", ctx);
|
|
134
|
+
if (governance.policies.enforce_ownership && module.owners.length === 0) {
|
|
135
|
+
ctx.addIssue({
|
|
136
|
+
code: z.ZodIssueCode.custom,
|
|
137
|
+
path: ["modules", moduleIndex, "owners"],
|
|
138
|
+
message: `模块 "${module.id}" 在 enforce_ownership=true 时必须声明 owner`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
module.roots.forEach((root, rootIndex) => {
|
|
142
|
+
const key = root.toLocaleLowerCase("en-US");
|
|
143
|
+
const owner = rootOwner.get(key);
|
|
144
|
+
if (owner && owner.moduleId !== module.id) {
|
|
145
|
+
ctx.addIssue({
|
|
146
|
+
code: z.ZodIssueCode.custom,
|
|
147
|
+
path: ["modules", moduleIndex, "roots", rootIndex],
|
|
148
|
+
message: `root "${root}" 已归属模块 "${owner.moduleId}"(modules.${owner.index}.roots)`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
else if (!owner) {
|
|
152
|
+
rootOwner.set(key, { moduleId: module.id, index: moduleIndex });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const dependencyFields = [
|
|
156
|
+
["depends_on", module.depends_on],
|
|
157
|
+
["allowed_dependencies", module.allowed_dependencies],
|
|
158
|
+
["denied_dependencies", module.denied_dependencies],
|
|
159
|
+
];
|
|
160
|
+
for (const [field, references] of dependencyFields) {
|
|
161
|
+
references.forEach((reference, referenceIndex) => {
|
|
162
|
+
if (!moduleIds.has(reference)) {
|
|
163
|
+
ctx.addIssue({
|
|
164
|
+
code: z.ZodIssueCode.custom,
|
|
165
|
+
path: ["modules", moduleIndex, field, referenceIndex],
|
|
166
|
+
message: `未知 module 引用 "${reference}"`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
else if (reference === module.id) {
|
|
170
|
+
ctx.addIssue({
|
|
171
|
+
code: z.ZodIssueCode.custom,
|
|
172
|
+
path: ["modules", moduleIndex, field, referenceIndex],
|
|
173
|
+
message: `模块 "${module.id}" 不能在 ${field} 中引用自身`,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const denied = new Set(module.denied_dependencies);
|
|
179
|
+
module.allowed_dependencies.forEach((dependency, dependencyIndex) => {
|
|
180
|
+
if (denied.has(dependency)) {
|
|
181
|
+
ctx.addIssue({
|
|
182
|
+
code: z.ZodIssueCode.custom,
|
|
183
|
+
path: ["modules", moduleIndex, "allowed_dependencies", dependencyIndex],
|
|
184
|
+
message: `依赖 "${dependency}" 不能同时允许和禁止`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
module.depends_on.forEach((dependency, dependencyIndex) => {
|
|
189
|
+
if (denied.has(dependency)) {
|
|
190
|
+
ctx.addIssue({
|
|
191
|
+
code: z.ZodIssueCode.custom,
|
|
192
|
+
path: ["modules", moduleIndex, "depends_on", dependencyIndex],
|
|
193
|
+
message: `已声明依赖 "${dependency}" 同时出现在 denied_dependencies`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
module.public_interfaces.forEach((interfaceId, interfaceIndex) => {
|
|
198
|
+
const item = interfaceById.get(interfaceId);
|
|
199
|
+
if (!item) {
|
|
200
|
+
ctx.addIssue({
|
|
201
|
+
code: z.ZodIssueCode.custom,
|
|
202
|
+
path: ["modules", moduleIndex, "public_interfaces", interfaceIndex],
|
|
203
|
+
message: `未知 interface 引用 "${interfaceId}"`,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
else if (item.provider !== module.id) {
|
|
207
|
+
ctx.addIssue({
|
|
208
|
+
code: z.ZodIssueCode.custom,
|
|
209
|
+
path: ["modules", moduleIndex, "public_interfaces", interfaceIndex],
|
|
210
|
+
message: `interface "${interfaceId}" 的 provider 是 "${item.provider}",不是 "${module.id}"`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
governance.interfaces.forEach((item, interfaceIndex) => {
|
|
216
|
+
if (!moduleIds.has(item.provider)) {
|
|
217
|
+
ctx.addIssue({
|
|
218
|
+
code: z.ZodIssueCode.custom,
|
|
219
|
+
path: ["interfaces", interfaceIndex, "provider"],
|
|
220
|
+
message: `未知 module 引用 "${item.provider}"`,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
addDuplicateListIssues(item.consumers, ["interfaces", interfaceIndex, "consumers"], "consumers", ctx);
|
|
224
|
+
addDuplicateListIssues(item.contract_files, ["interfaces", interfaceIndex, "contract_files"], "contract_files", ctx);
|
|
225
|
+
item.consumers.forEach((consumer, consumerIndex) => {
|
|
226
|
+
if (!moduleIds.has(consumer)) {
|
|
227
|
+
ctx.addIssue({
|
|
228
|
+
code: z.ZodIssueCode.custom,
|
|
229
|
+
path: ["interfaces", interfaceIndex, "consumers", consumerIndex],
|
|
230
|
+
message: `未知 module 引用 "${consumer}"`,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
if (governance.policies.enforce_public_interfaces) {
|
|
235
|
+
const provider = governance.modules.find((module) => module.id === item.provider);
|
|
236
|
+
if (provider && !provider.public_interfaces.includes(item.id)) {
|
|
237
|
+
ctx.addIssue({
|
|
238
|
+
code: z.ZodIssueCode.custom,
|
|
239
|
+
path: ["interfaces", interfaceIndex, "provider"],
|
|
240
|
+
message: `interface "${item.id}" 未登记在 provider "${item.provider}" 的 public_interfaces`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
governance.repositories.forEach((repository, repositoryIndex) => {
|
|
246
|
+
const rootKey = repository.root.toLocaleLowerCase("en-US");
|
|
247
|
+
const owner = repositoryRootOwner.get(rootKey);
|
|
248
|
+
if (owner && owner.repositoryId !== repository.id) {
|
|
249
|
+
ctx.addIssue({
|
|
250
|
+
code: z.ZodIssueCode.custom,
|
|
251
|
+
path: ["repositories", repositoryIndex, "root"],
|
|
252
|
+
message: `repository root "${repository.root}" 已归属仓库 "${owner.repositoryId}"(repositories.${owner.index}.root)`,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
else if (!owner) {
|
|
256
|
+
repositoryRootOwner.set(rootKey, { repositoryId: repository.id, index: repositoryIndex });
|
|
257
|
+
}
|
|
258
|
+
addDuplicateListIssues(repository.dependencies.map((dependency) => dependency.repository), ["repositories", repositoryIndex, "dependencies"], "dependencies", ctx);
|
|
259
|
+
repository.dependencies.forEach((dependency, dependencyIndex) => {
|
|
260
|
+
// Repository dependencies are portfolio-wide references: the target may
|
|
261
|
+
// live in another governance file, so missing-target checks belong to
|
|
262
|
+
// the composition layer rather than this single-file schema.
|
|
263
|
+
if (dependency.repository === repository.id) {
|
|
264
|
+
ctx.addIssue({
|
|
265
|
+
code: z.ZodIssueCode.custom,
|
|
266
|
+
path: ["repositories", repositoryIndex, "dependencies", dependencyIndex, "repository"],
|
|
267
|
+
message: `仓库 "${repository.id}" 不能依赖自身`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
export function governancePath(root) {
|
|
274
|
+
return pmPath(path.resolve(root), GOVERNANCE_FILE);
|
|
275
|
+
}
|
|
276
|
+
export function loadGovernance(root) {
|
|
277
|
+
const file = governancePath(root);
|
|
278
|
+
if (!fs.existsSync(file)) {
|
|
279
|
+
throw new Error(`治理模型未初始化(缺少 ${file}),请先调用 init_governance。`);
|
|
280
|
+
}
|
|
281
|
+
const governance = loadJson(file, GovernanceSchema);
|
|
282
|
+
if (governance === undefined) {
|
|
283
|
+
throw new Error(`治理模型未初始化(缺少 ${file}),请先调用 init_governance。`);
|
|
284
|
+
}
|
|
285
|
+
return governance;
|
|
286
|
+
}
|
|
287
|
+
export function saveGovernance(root, governance) {
|
|
288
|
+
const parsed = GovernanceSchema.parse(governance);
|
|
289
|
+
const file = governancePath(root);
|
|
290
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
291
|
+
saveJson(file, parsed);
|
|
292
|
+
return parsed;
|
|
293
|
+
}
|
|
294
|
+
export function initGovernance(root, governance = {}) {
|
|
295
|
+
return withLedgerLock(root, () => {
|
|
296
|
+
const file = governancePath(root);
|
|
297
|
+
if (fs.existsSync(file)) {
|
|
298
|
+
throw new Error(`治理模型已初始化(${file} 已存在)。`);
|
|
299
|
+
}
|
|
300
|
+
return saveGovernance(root, governance);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/** Fail-closed existence and schema check for callers that require governance. */
|
|
304
|
+
export function ensureGovernance(root) {
|
|
305
|
+
return loadGovernance(root);
|
|
306
|
+
}
|