@haiyangbg/buildbeat 0.0.0 → 1.21.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/CHANGELOG.md +304 -0
- package/LICENSE +21 -0
- package/README.en.md +290 -0
- package/README.md +285 -4
- package/SKILL.md +334 -0
- package/bin/buildbeat.js +5 -0
- package/bin/solobaton.js +6 -0
- package/docs/CAPABILITY-MATRIX.md +50 -0
- package/docs/CHECKS.md +326 -0
- package/docs/CLI-PILOT-2026-08-23.md +25 -0
- package/docs/CLI-STRATEGY-2026-08.md +55 -0
- package/docs/CLI.md +233 -0
- package/docs/EXECUTION-PLAN.md +487 -0
- package/docs/LEGACY-V1.16-MIGRATION.md +54 -0
- package/docs/PHASE1-PILOT-2026-08-24.md +32 -0
- package/docs/PHASE2-BUILDBEAT-PILOT-2026-08-25.md +75 -0
- package/docs/PHASE2-PILOT-2026-08-25.md +88 -0
- package/docs/PHASE2-PILOT-PREFLIGHT-2026-08-25.md +42 -0
- package/docs/PHASE4-STABILITY-AUDIT-2026-08-25.md +35 -0
- package/docs/PHASE4-V1.20-PILOT-2026-08-25.md +56 -0
- package/docs/RELEASING.md +117 -0
- package/docs/ROADMAP.md +872 -0
- package/docs/WP4.3-RELEASE-EVIDENCE-2026-08-25.md +73 -0
- package/example/.buildbeat/manifest.json +45 -0
- package/example/AGENTS.md +19 -0
- package/example/ARCHITECTURE.md +39 -0
- package/example/BUILDBEAT.md +17 -0
- package/example/CLAUDE.md +7 -0
- package/example/README.md +75 -0
- package/example/contracts/PROTOCOL.md +38 -0
- package/example/pm/NOW.md +22 -0
- package/example/pm/adr/ADR-0001-local-first-sqlite.md +25 -0
- package/example/pm/adr/README.md +7 -0
- package/example/pm/archive//344/270/200/346/234/237/evidence/gate1.md +5 -0
- package/example/pm/archive//344/270/200/346/234/237/evidence/gate2.md +5 -0
- package/example/pm/archive//344/270/200/346/234/237/evidence/gate3.md +5 -0
- package/example/pm/archive//344/270/200/346/234/237/evidence/gate4.md +5 -0
- package/example/pm/archive//344/270/200/346/234/237/evidence/implementation.md +5 -0
- package/example/pm/decisions.md +20 -0
- package/example/pm/status//344/272/247/345/223/201.md +20 -0
- package/example/pm/status//345/205/250/346/240/210.md +15 -0
- package/example/pm/status//346/265/213/350/257/225.md +15 -0
- package/example/pm//344/270/200/346/234/237-/347/234/213/346/235/277.md +97 -0
- package/example/standards/CODE.md +18 -0
- package/example/standards/DESIGN.md +34 -0
- package/example/standards/REVIEW.md +16 -0
- package/example/standards/STACK.md +31 -0
- package/lessons.md +127 -0
- package/package.json +48 -7
- package/src/cli.js +323 -0
- package/src/constants.js +199 -0
- package/src/doctor.js +267 -0
- package/src/planner.js +251 -0
- package/src/project.js +839 -0
- package/src/upgrader.js +1249 -0
- package/src/writer.js +534 -0
- package/templates/.claude/agents/reviewer.md +62 -0
- package/templates/AGENTS.md +85 -0
- package/templates/ARCHITECTURE.md +50 -0
- package/templates/BUILDBEAT.md +13 -0
- package/templates/CLAUDE.md +7 -0
- package/templates/contracts/PROTOCOL.md +32 -0
- package/templates/gitignore.template +19 -0
- package/templates/pm/NOW.md +26 -0
- package/templates/pm/adr/ADR-0000-template.md +25 -0
- package/templates/pm/adr/README.md +15 -0
- package/templates/pm/changes/README.md +44 -0
- package/templates/pm/decisions.md +12 -0
- package/templates/pm/status/README.md +32 -0
- package/templates/pm//345/275/223/346/234/237/347/234/213/346/235/277.md +62 -0
- package/templates/scripts/bus-check.sh +1850 -0
- package/templates/scripts/design-preview.sh +44 -0
- package/templates/scripts/drift-check.sh +112 -0
- package/templates/scripts/pre-commit.sh +74 -0
- package/templates/scripts/verify-status.sh +105 -0
- package/templates/standards/CODE.md +23 -0
- package/templates/standards/DESIGN.md +36 -0
- package/templates/standards/REVIEW.md +20 -0
- package/templates/standards/STACK.md +37 -0
- package/templates//346/214/207/346/214/245/345/217/260.md +58 -0
package/src/constants.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const packageJson = JSON.parse(
|
|
4
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
export const CLI_VERSION = packageJson.version;
|
|
8
|
+
export const SCAFFOLD_VERSION = `v${CLI_VERSION.split(".").slice(0, 2).join(".")}`;
|
|
9
|
+
export const OUTPUT_SCHEMA_VERSION = 2;
|
|
10
|
+
export const PRODUCT_NAME = "BuildBeat";
|
|
11
|
+
export const LEGACY_PRODUCT_NAME = "Solobaton";
|
|
12
|
+
export const MARKER_FILE = "BUILDBEAT.md";
|
|
13
|
+
export const LEGACY_MARKER_FILE = "SOLOBATON.md";
|
|
14
|
+
export const MANIFEST_PATH = ".buildbeat/manifest.json";
|
|
15
|
+
export const LEGACY_MANIFEST_PATH = ".solobaton/manifest.json";
|
|
16
|
+
export const MANIFEST_PATHS = [MANIFEST_PATH, LEGACY_MANIFEST_PATH];
|
|
17
|
+
export const MANIFEST_SCHEMA_VERSION = 2;
|
|
18
|
+
export const SUPPORTED_MANIFEST_SCHEMA_VERSIONS = [1, 2];
|
|
19
|
+
|
|
20
|
+
export const GITIGNORE_BEGIN_MARKER = "# >>> buildbeat managed >>>";
|
|
21
|
+
export const GITIGNORE_END_MARKER = "# <<< buildbeat managed <<<";
|
|
22
|
+
export const LEGACY_GITIGNORE_BEGIN_MARKER = "# >>> solobaton managed >>>";
|
|
23
|
+
export const LEGACY_GITIGNORE_END_MARKER = "# <<< solobaton managed <<<";
|
|
24
|
+
export const GITIGNORE_MARKER_PAIRS = [
|
|
25
|
+
[GITIGNORE_BEGIN_MARKER, GITIGNORE_END_MARKER],
|
|
26
|
+
[LEGACY_GITIGNORE_BEGIN_MARKER, LEGACY_GITIGNORE_END_MARKER],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export const SCRIPT_NAMES = [
|
|
30
|
+
"bus-check.sh",
|
|
31
|
+
"design-preview.sh",
|
|
32
|
+
"drift-check.sh",
|
|
33
|
+
"pre-commit.sh",
|
|
34
|
+
"verify-status.sh",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export const COMMON_REQUIRED_FILES = [
|
|
38
|
+
".claude/agents/reviewer.md",
|
|
39
|
+
"AGENTS.md",
|
|
40
|
+
"ARCHITECTURE.md",
|
|
41
|
+
"CLAUDE.md",
|
|
42
|
+
"contracts/PROTOCOL.md",
|
|
43
|
+
"pm/NOW.md",
|
|
44
|
+
"pm/changes/README.md",
|
|
45
|
+
"pm/decisions.md",
|
|
46
|
+
"pm/status/README.md",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// Optional, project-owned template libraries. They ship in the package so an
|
|
50
|
+
// AI session can use them on demand, but they are never part of the default
|
|
51
|
+
// init/adopt plan or the required scaffold surface.
|
|
52
|
+
export const OPTIONAL_TEMPLATE_PREFIXES = ["standards/", "pm/adr/"];
|
|
53
|
+
|
|
54
|
+
export const IGNORED_SCAN_DIRECTORIES = new Set([
|
|
55
|
+
".claude",
|
|
56
|
+
".codex",
|
|
57
|
+
".git",
|
|
58
|
+
".buildbeat",
|
|
59
|
+
".solobaton",
|
|
60
|
+
".next",
|
|
61
|
+
".nuxt",
|
|
62
|
+
".output",
|
|
63
|
+
"build",
|
|
64
|
+
"coverage",
|
|
65
|
+
"dist",
|
|
66
|
+
"node_modules",
|
|
67
|
+
"target",
|
|
68
|
+
"vendor",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
export const RENDER_REQUIRED_PLACEHOLDERS = {
|
|
72
|
+
".claude/agents/reviewer.md": ["<项目名>"],
|
|
73
|
+
"AGENTS.md": [
|
|
74
|
+
"<项目名>",
|
|
75
|
+
"<N>",
|
|
76
|
+
"<代码仓1>",
|
|
77
|
+
"<代码仓2>",
|
|
78
|
+
"<被测仓>",
|
|
79
|
+
"<PreStop/drain 机制>",
|
|
80
|
+
],
|
|
81
|
+
"ARCHITECTURE.md": [
|
|
82
|
+
"<项目名>",
|
|
83
|
+
"<项目是什么、给谁用、核心取舍>",
|
|
84
|
+
"<用户入口>",
|
|
85
|
+
"<前端/客户端>",
|
|
86
|
+
"<仓名>",
|
|
87
|
+
"<部署单元>",
|
|
88
|
+
"<网关/服务A>",
|
|
89
|
+
"<审计/可观测>",
|
|
90
|
+
"<服务B>",
|
|
91
|
+
"<服务C>",
|
|
92
|
+
"<项目根>",
|
|
93
|
+
"<代码仓1>",
|
|
94
|
+
"<代码仓2>",
|
|
95
|
+
"<说明>",
|
|
96
|
+
"<部署平台/区域>",
|
|
97
|
+
"<应用/服务清单:名称+ID+实例>",
|
|
98
|
+
"<入口/域名/LB>",
|
|
99
|
+
"<数据库/缓存>",
|
|
100
|
+
"<服务A 凭据>",
|
|
101
|
+
"<路径/获取命令>",
|
|
102
|
+
"<运行时 env 实查>",
|
|
103
|
+
"<平台 CLI 命令>",
|
|
104
|
+
],
|
|
105
|
+
"BUILDBEAT.md": ["<X.Y>", "<yyyy-mm-dd>", "<默认|紧凑>"],
|
|
106
|
+
"contracts/PROTOCOL.md": [
|
|
107
|
+
"<项目名>",
|
|
108
|
+
"<vX.Y.Z>",
|
|
109
|
+
"<上线日期>",
|
|
110
|
+
"<代码子仓1>",
|
|
111
|
+
"<bus-baseline.json app 名或 n/a>",
|
|
112
|
+
"<边界A:服务X ↔ 服务Y>",
|
|
113
|
+
"<对齐点>",
|
|
114
|
+
"<怎么核的:实测命令/代码行/配置查询>",
|
|
115
|
+
],
|
|
116
|
+
"gitignore.template": ["<代码仓1>", "<代码仓2>"],
|
|
117
|
+
"pm/NOW.md": ["<期名>", "<一句话状态>", "<快轨|标准轨|重轨>", "<期>", "<域>"],
|
|
118
|
+
"pm/decisions.md": [
|
|
119
|
+
"<yyyy-mm-dd>",
|
|
120
|
+
"<谁>",
|
|
121
|
+
"<包ID>",
|
|
122
|
+
"<默认 2–5 个真实取舍的收敛结论;单项决定直接写一句>",
|
|
123
|
+
"<文件/落点>",
|
|
124
|
+
],
|
|
125
|
+
"pm/当期看板.md": [
|
|
126
|
+
"<期名>",
|
|
127
|
+
"<工作包名>",
|
|
128
|
+
"<这轮最终要交付什么结果,不用文件名代替>",
|
|
129
|
+
"<产品 / 全栈 / 测试;按需调用,各自仍只写自己的边界>",
|
|
130
|
+
"<为达成结果可自动继续的关联任务/AI视角/文件边界>",
|
|
131
|
+
"<带证据完成 / 必须由人处理的真实阻塞 / 用户明确的检查点>",
|
|
132
|
+
"<完成时填一条可核验的仓库相对路径 / candidate hash;未完成留空>",
|
|
133
|
+
"<N>",
|
|
134
|
+
"<Gate>",
|
|
135
|
+
"<需求1>",
|
|
136
|
+
"<跨边界接口/字段/机制 约定;契约口径落 `contracts/PROTOCOL.md`>",
|
|
137
|
+
"<对照需求清单 + 设计稿逐屏;上线前必过的清单>",
|
|
138
|
+
],
|
|
139
|
+
"pm/adr/ADR-0000-template.md": [
|
|
140
|
+
"<标题>",
|
|
141
|
+
"<yyyy-mm-dd>",
|
|
142
|
+
"<为什么现在必须做这个长期技术决定>",
|
|
143
|
+
"<最终选择及其边界>",
|
|
144
|
+
"<正向、负向与回滚影响>",
|
|
145
|
+
"<方案 A / B / C 及未选原因>",
|
|
146
|
+
"<工作包 / 契约 / evidence 路径或 n/a>",
|
|
147
|
+
],
|
|
148
|
+
"scripts/drift-check.sh": ["<应用1>", "<应用2>", "<代码子仓1>"],
|
|
149
|
+
"scripts/verify-status.sh": ["<套件1>", "<套件2>", "<代码子仓1>", "<代码子仓2>"],
|
|
150
|
+
"standards/STACK.md": [
|
|
151
|
+
"<项目名>",
|
|
152
|
+
"<运行时及版本约束>",
|
|
153
|
+
"<包管理器及 lockfile>",
|
|
154
|
+
"<主要语言与框架>",
|
|
155
|
+
"<数据库 / 缓存 / 消息设施>",
|
|
156
|
+
"<部署平台 / 容器基线>",
|
|
157
|
+
"<CI 与测试命令>",
|
|
158
|
+
"<许可证 / 供应链约束>",
|
|
159
|
+
"<.nvmrc / engines.node 的精确值;多值重复本行;无则 n/a>",
|
|
160
|
+
"<lockfile 文件名;多类重复本行;无则 n/a>",
|
|
161
|
+
"<Dockerfile FROM 镜像;多值重复本行;无则 n/a>",
|
|
162
|
+
],
|
|
163
|
+
"standards/CODE.md": ["<项目名>", "<代码组织与命名约定>", "<项目特有禁止事项>"],
|
|
164
|
+
"standards/REVIEW.md": ["<项目名>", "<项目特有 Review 条件>"],
|
|
165
|
+
"standards/DESIGN.md": [
|
|
166
|
+
"<项目名>",
|
|
167
|
+
"<设计原则>",
|
|
168
|
+
"<排版 / 色彩 / 间距 token 来源>",
|
|
169
|
+
"<核心组件与复用边界>",
|
|
170
|
+
"<项目特有例外>",
|
|
171
|
+
],
|
|
172
|
+
"指挥台.md": ["<被测仓>"],
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const FILE_POLICIES = {
|
|
176
|
+
REPLACE_IF_UNMODIFIED: "replace-if-unmodified",
|
|
177
|
+
// Schema 1 compatibility only. New plans and schema 2 manifests never emit
|
|
178
|
+
// this historical policy.
|
|
179
|
+
THREE_WAY_ONLY: "three-way-only",
|
|
180
|
+
PROJECT_OWNED: "project-owned",
|
|
181
|
+
MERGE_ONLY: "merge-only",
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export function filePolicy(templatePath) {
|
|
185
|
+
if (templatePath === "gitignore.template") {
|
|
186
|
+
return FILE_POLICIES.MERGE_ONLY;
|
|
187
|
+
}
|
|
188
|
+
if (
|
|
189
|
+
templatePath === "CLAUDE.md" ||
|
|
190
|
+
templatePath === ".claude/agents/reviewer.md" ||
|
|
191
|
+
templatePath === "AGENTS.md" ||
|
|
192
|
+
templatePath === "BUILDBEAT.md" ||
|
|
193
|
+
templatePath === "指挥台.md" ||
|
|
194
|
+
(/^scripts\//.test(templatePath) && templatePath !== "scripts/verify-status.sh")
|
|
195
|
+
) {
|
|
196
|
+
return FILE_POLICIES.REPLACE_IF_UNMODIFIED;
|
|
197
|
+
}
|
|
198
|
+
return FILE_POLICIES.PROJECT_OWNED;
|
|
199
|
+
}
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CLI_VERSION,
|
|
6
|
+
COMMON_REQUIRED_FILES,
|
|
7
|
+
OUTPUT_SCHEMA_VERSION,
|
|
8
|
+
SCRIPT_NAMES,
|
|
9
|
+
SUPPORTED_MANIFEST_SCHEMA_VERSIONS,
|
|
10
|
+
} from "./constants.js";
|
|
11
|
+
import { inspectProject } from "./project.js";
|
|
12
|
+
|
|
13
|
+
const LEVEL_ORDER = { error: 0, warning: 1, info: 2 };
|
|
14
|
+
|
|
15
|
+
function add(findings, level, code, message, relativePath = null) {
|
|
16
|
+
findings.push({ level, code, message, path: relativePath });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function requiredFiles(installation) {
|
|
20
|
+
const scriptBase = installation.layout === "compact" ? "pm/scripts" : "scripts";
|
|
21
|
+
const operatorCard = installation.layout === "compact" ? "pm/指挥台.md" : "指挥台.md";
|
|
22
|
+
const localFiles = [installation.markerPath, operatorCard].filter(Boolean);
|
|
23
|
+
return [
|
|
24
|
+
...COMMON_REQUIRED_FILES,
|
|
25
|
+
...localFiles,
|
|
26
|
+
...SCRIPT_NAMES.map((name) => `${scriptBase}/${name}`),
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function verifyStatusHasPlaceholders(target, layout) {
|
|
31
|
+
const relative = layout === "compact"
|
|
32
|
+
? "pm/scripts/verify-status.sh"
|
|
33
|
+
: "scripts/verify-status.sh";
|
|
34
|
+
const filename = path.join(target, relative);
|
|
35
|
+
if (!existsSync(filename)) {
|
|
36
|
+
return { relative, unconfigured: false };
|
|
37
|
+
}
|
|
38
|
+
const text = readFileSync(filename, "utf8");
|
|
39
|
+
return { relative, unconfigured: text.includes("<套件1>") || text.includes("<代码子仓1>") };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function runDoctor(targetInput) {
|
|
43
|
+
const inspection = inspectProject(targetInput);
|
|
44
|
+
const findings = [];
|
|
45
|
+
const { installation } = inspection;
|
|
46
|
+
|
|
47
|
+
if (!inspection.exists) {
|
|
48
|
+
add(findings, "error", "target.not_found", "Target directory does not exist.");
|
|
49
|
+
} else if (installation.state === "not-installed") {
|
|
50
|
+
add(findings, "error", "install.not_found", "No BuildBeat or legacy Solobaton installation was detected.");
|
|
51
|
+
} else if (installation.state === "mixed") {
|
|
52
|
+
add(
|
|
53
|
+
findings,
|
|
54
|
+
"error",
|
|
55
|
+
"install.mixed_layout",
|
|
56
|
+
"Multiple BuildBeat/legacy markers or default/compact layout signals coexist; ownership is ambiguous.",
|
|
57
|
+
);
|
|
58
|
+
} else if (installation.state === "partial") {
|
|
59
|
+
add(
|
|
60
|
+
findings,
|
|
61
|
+
"error",
|
|
62
|
+
"install.partial",
|
|
63
|
+
`A partial ${installation.layout} layout was detected without a version marker.`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (installation.state === "installed") {
|
|
68
|
+
if (!installation.version) {
|
|
69
|
+
add(findings, "error", "version.unreadable", `${installation.markerPath} has no readable installed version.`);
|
|
70
|
+
}
|
|
71
|
+
for (const relative of requiredFiles(installation)) {
|
|
72
|
+
if (!existsSync(path.join(inspection.target, relative))) {
|
|
73
|
+
add(findings, "error", "file.missing", "Required scaffold file is missing.", relative);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (installation.namespace === "solobaton") {
|
|
77
|
+
add(
|
|
78
|
+
findings,
|
|
79
|
+
"info",
|
|
80
|
+
"install.legacy_namespace",
|
|
81
|
+
"A legacy Solobaton marker was detected; it remains readable, while new scaffolds use BuildBeat identifiers.",
|
|
82
|
+
installation.markerPath,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const verify = verifyStatusHasPlaceholders(inspection.target, installation.layout);
|
|
86
|
+
if (verify.unconfigured) {
|
|
87
|
+
add(
|
|
88
|
+
findings,
|
|
89
|
+
"warning",
|
|
90
|
+
"verification.unconfigured",
|
|
91
|
+
"verify-status still contains suite placeholders; L3 automation is not configured.",
|
|
92
|
+
verify.relative,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (inspection.manifest.state === "missing" && installation.state === "installed") {
|
|
98
|
+
add(
|
|
99
|
+
findings,
|
|
100
|
+
"warning",
|
|
101
|
+
"manifest.missing",
|
|
102
|
+
"This is a legacy/unmanaged installation; safe automated upgrade and uninstall are unavailable.",
|
|
103
|
+
inspection.manifest.path,
|
|
104
|
+
);
|
|
105
|
+
} else if (inspection.manifest.state === "ambiguous") {
|
|
106
|
+
add(
|
|
107
|
+
findings,
|
|
108
|
+
"error",
|
|
109
|
+
"manifest.ambiguous",
|
|
110
|
+
"Both BuildBeat and legacy Solobaton lifecycle manifests exist; ownership is ambiguous.",
|
|
111
|
+
inspection.manifest.path,
|
|
112
|
+
);
|
|
113
|
+
} else if (inspection.manifest.state === "invalid") {
|
|
114
|
+
add(findings, "error", "manifest.invalid", "The lifecycle manifest is unreadable or invalid JSON.", inspection.manifest.path);
|
|
115
|
+
} else if (
|
|
116
|
+
inspection.manifest.state === "present" &&
|
|
117
|
+
inspection.manifest.schemaVersion !== null &&
|
|
118
|
+
!SUPPORTED_MANIFEST_SCHEMA_VERSIONS.includes(inspection.manifest.schemaVersion)
|
|
119
|
+
) {
|
|
120
|
+
add(
|
|
121
|
+
findings,
|
|
122
|
+
"error",
|
|
123
|
+
"manifest.unsupported_schema",
|
|
124
|
+
`Manifest schema ${inspection.manifest.schemaVersion ?? "unknown"} is not supported.`,
|
|
125
|
+
inspection.manifest.path,
|
|
126
|
+
);
|
|
127
|
+
} else if (inspection.manifest.state === "present") {
|
|
128
|
+
for (const issue of inspection.manifest.validationIssues) {
|
|
129
|
+
add(findings, "error", issue.code, issue.message, inspection.manifest.path);
|
|
130
|
+
}
|
|
131
|
+
if (
|
|
132
|
+
installation.state === "installed" &&
|
|
133
|
+
["default", "compact"].includes(inspection.manifest.layout) &&
|
|
134
|
+
inspection.manifest.layout !== installation.layout
|
|
135
|
+
) {
|
|
136
|
+
add(
|
|
137
|
+
findings,
|
|
138
|
+
"error",
|
|
139
|
+
"manifest.layout_mismatch",
|
|
140
|
+
"Manifest layout does not match the detected installation layout.",
|
|
141
|
+
inspection.manifest.path,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (
|
|
145
|
+
installation.state === "installed" &&
|
|
146
|
+
installation.version &&
|
|
147
|
+
/^v\d+\.\d+(?:\.\d+)?$/.test(inspection.manifest.scaffoldVersion || "") &&
|
|
148
|
+
inspection.manifest.scaffoldVersion !== installation.version
|
|
149
|
+
) {
|
|
150
|
+
add(
|
|
151
|
+
findings,
|
|
152
|
+
"error",
|
|
153
|
+
"manifest.version_mismatch",
|
|
154
|
+
`Manifest scaffoldVersion does not match ${installation.markerPath}.`,
|
|
155
|
+
inspection.manifest.path,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const item of inspection.placeholders) {
|
|
161
|
+
add(
|
|
162
|
+
findings,
|
|
163
|
+
"warning",
|
|
164
|
+
"placeholder.remaining",
|
|
165
|
+
`Canonical placeholders remain: ${item.tokens.join(", ")}`,
|
|
166
|
+
item.path,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (inspection.exists && !inspection.signals.hasGit) {
|
|
171
|
+
add(findings, "error", "git.not_initialized", "The target has no root .git entry.");
|
|
172
|
+
}
|
|
173
|
+
if (inspection.exists && inspection.hook.mode === "direct-non-executable") {
|
|
174
|
+
add(
|
|
175
|
+
findings,
|
|
176
|
+
"warning",
|
|
177
|
+
"hook.non_executable",
|
|
178
|
+
"The root pre-commit hook exists but is not executable.",
|
|
179
|
+
inspection.hook.path,
|
|
180
|
+
);
|
|
181
|
+
} else if (inspection.exists && !inspection.hook.configured) {
|
|
182
|
+
add(
|
|
183
|
+
findings,
|
|
184
|
+
"warning",
|
|
185
|
+
"hook.missing",
|
|
186
|
+
"No root pre-commit hook or core.hooksPath configuration was detected.",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const command of ["git", "bash"]) {
|
|
191
|
+
if (!inspection.dependencies[command].available) {
|
|
192
|
+
add(findings, "error", `dependency.${command}_missing`, `${command} is required but unavailable.`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!inspection.dependencies.gitleaks.available) {
|
|
196
|
+
add(
|
|
197
|
+
findings,
|
|
198
|
+
"warning",
|
|
199
|
+
"dependency.gitleaks_missing",
|
|
200
|
+
"gitleaks is unavailable; the local Secret gate degrades to a warning.",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (!inspection.dependencies.python3.available) {
|
|
204
|
+
add(findings, "info", "dependency.python_missing", "Python 3 is unavailable; design preview cannot run.");
|
|
205
|
+
}
|
|
206
|
+
if (!inspection.dependencies.jq.available) {
|
|
207
|
+
add(findings, "info", "dependency.jq_missing", "jq is unavailable; production drift checks cannot run.");
|
|
208
|
+
}
|
|
209
|
+
if (inspection.scan.truncated) {
|
|
210
|
+
add(
|
|
211
|
+
findings,
|
|
212
|
+
"warning",
|
|
213
|
+
"scan.truncated",
|
|
214
|
+
"Project scan hit the 5,000-entry safety limit; results are incomplete.",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
for (const item of inspection.scan.warnings) {
|
|
218
|
+
add(findings, "warning", "scan.unreadable", `Path could not be inspected: ${item.reason}.`, item.path);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
findings.sort((a, b) =>
|
|
222
|
+
LEVEL_ORDER[a.level] - LEVEL_ORDER[b.level] ||
|
|
223
|
+
a.code.localeCompare(b.code) ||
|
|
224
|
+
(a.path || "").localeCompare(b.path || ""),
|
|
225
|
+
);
|
|
226
|
+
const summary = {
|
|
227
|
+
errors: findings.filter((item) => item.level === "error").length,
|
|
228
|
+
warnings: findings.filter((item) => item.level === "warning").length,
|
|
229
|
+
info: findings.filter((item) => item.level === "info").length,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
schemaVersion: OUTPUT_SCHEMA_VERSION,
|
|
234
|
+
command: "doctor",
|
|
235
|
+
cliVersion: CLI_VERSION,
|
|
236
|
+
target: inspection.target,
|
|
237
|
+
installation: inspection.installation,
|
|
238
|
+
manifest: inspection.manifest,
|
|
239
|
+
capabilities: inspection.dependencies,
|
|
240
|
+
findings,
|
|
241
|
+
summary,
|
|
242
|
+
ok: summary.errors === 0,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function formatDoctor(report) {
|
|
247
|
+
const lines = [
|
|
248
|
+
"BuildBeat doctor",
|
|
249
|
+
`Target: ${report.target}`,
|
|
250
|
+
`Installation: ${report.installation.state}${report.installation.layout ? ` (${report.installation.layout})` : ""}${report.installation.version ? ` ${report.installation.version}` : ""}`,
|
|
251
|
+
`Manifest: ${report.manifest.state}`,
|
|
252
|
+
"",
|
|
253
|
+
];
|
|
254
|
+
if (report.findings.length === 0) {
|
|
255
|
+
lines.push("OK No findings.");
|
|
256
|
+
} else {
|
|
257
|
+
for (const finding of report.findings) {
|
|
258
|
+
const location = finding.path ? ` [${finding.path}]` : "";
|
|
259
|
+
lines.push(`${finding.level.toUpperCase()} ${finding.code}${location}: ${finding.message}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
lines.push(
|
|
263
|
+
"",
|
|
264
|
+
`Result: ${report.summary.errors} error(s), ${report.summary.warnings} warning(s), ${report.summary.info} info item(s).`,
|
|
265
|
+
);
|
|
266
|
+
return lines.join("\n");
|
|
267
|
+
}
|