diff --git a/.Knowledge/index.md b/.Knowledge/index.md index 69f0429..3b61213 100644 --- a/.Knowledge/index.md +++ b/.Knowledge/index.md @@ -37,6 +37,7 @@ | f2s-dev-workflow-constraints | `.Knowledge/topics/f2s-dev-workflow-constraints.md` | 开发工作流约束(仅 Flow2Spec 本仓自身,不给下游):只改 templates / 不改配置根 / 用户驱动分发 | 长文:`rules/repo-dev-workflow-constraints.*`;自查技能:`skills/repo-dev-check/SKILL.md` | | f2s-kb-distill | `.Knowledge/topics/f2s-kb-distill.md` | Q&A 驱动知识提取;自动判断创建新 topic 或追加现有 topic | 技能:skills/f2s-kb-distill/SKILL.mdc | | flow2spec-init-defaults | `.Knowledge/topics/f2s-init-defaults.md` | `flow2spec init` 字段默认值、四处落点一致性、老项目缺字段补写、`init` 不动 stock/req/topics/matchers、manifest 两个版本字段(`projectRev` / `pkgRev`)对照 | 包源码:`lib/flow2specConfig.js` `DEFAULTS` / `CONFIG_FIELDS`;模板:`templates/{locale}/flow2spec.config.json` | +| flow2spec-doctor | `.Knowledge/topics/flow2spec-doctor.md` | `flow2spec doctor` 只读检查环境、项目初始化、协作上下文与知识库健康 | stock:[Doctor 诊断命令](stock-docs/Flow2Spec-doctor诊断命令.md);中英文命令文档:`docs/命令说明.md` / `docs/en/commands-reference.md` | 每主题保留 **1–3 条** 可点击摘要链接;全量路径对照写入 `.Knowledge/migration-report.md`(迁移场景)。 其中 **`implement-tech-design`**、**`f2s-doc-routing`**、**`config-precheck`**、**`f2s-task`** 在 `topics/` 内为**路由摘要**;执行长文见配置根 **`rules/f2s-*.md(c)`**;使用 Codex 时见 **`.codex/AGENTS.md`**、**`.codex/topics/f2s-*.md`**(`f2s-config-check` 与 `AGENTS` 前置同源,按需打开)。**`f2s-knowledge-preflight`** 与 **`f2s-kb-feedback-closing`** 是普通问答首读 / 源码补答收口门禁,作为配置根规则 / Codex 专题长文生效,不写入 `topicPaths` 或 `taskToTopicRules`;**`f2s-kb-feedback-closing`** 在 cases 1–3 建议执行 **`f2s-kb-distill`**。 diff --git a/.Knowledge/manifest-routing.json b/.Knowledge/manifest-routing.json index b0f9ae8..5159835 100644 --- a/.Knowledge/manifest-routing.json +++ b/.Knowledge/manifest-routing.json @@ -32,7 +32,8 @@ "f2s-dev-workflow-constraints": ".Knowledge/topics/f2s-dev-workflow-constraints.md", "f2s-kb-distill": ".Knowledge/topics/f2s-kb-distill.md", "flow2spec-init-defaults": ".Knowledge/topics/f2s-init-defaults.md", - "flow2spec-collaboration": ".Knowledge/topics/flow2spec-collaboration.md" + "flow2spec-collaboration": ".Knowledge/topics/flow2spec-collaboration.md", + "flow2spec-doctor": ".Knowledge/topics/flow2spec-doctor.md" }, "taskToTopicRules": [ { @@ -149,6 +150,14 @@ "topics": [ "flow2spec-collaboration" ] + }, + { + "task": "flow2spec-doctor", + "matcherId": "m-flow2spec-doctor", + "matcherPath": ".Knowledge/matchers/m-flow2spec-doctor.json", + "topics": [ + "flow2spec-doctor" + ] } ], "pkgRev": 1, @@ -221,6 +230,13 @@ "tags": [ "policy" ] + }, + "flow2spec-doctor": { + "primary": "feature", + "confidence": "inferred", + "tags": [ + "module" + ] } } } diff --git a/.Knowledge/matchers/m-flow2spec-doctor.json b/.Knowledge/matchers/m-flow2spec-doctor.json new file mode 100644 index 0000000..bb7f998 --- /dev/null +++ b/.Knowledge/matchers/m-flow2spec-doctor.json @@ -0,0 +1,16 @@ +{ + "includeAny": [ + "flow2spec doctor", + "doctor 命令", + "环境诊断", + "项目健康检查", + "安装检查", + "配置检查", + "Flow2Spec 诊断", + "TASK_ROOT 检查", + "知识库健康" + ], + "id": "m-flow2spec-doctor", + "version": "1.0.0", + "schema": "flow2spec.matcher.v1" +} diff --git "a/.Knowledge/stock-docs/Flow2Spec-doctor\350\257\212\346\226\255\345\221\275\344\273\244.md" "b/.Knowledge/stock-docs/Flow2Spec-doctor\350\257\212\346\226\255\345\221\275\344\273\244.md" new file mode 100644 index 0000000..cea4e06 --- /dev/null +++ "b/.Knowledge/stock-docs/Flow2Spec-doctor\350\257\212\346\226\255\345\221\275\344\273\244.md" @@ -0,0 +1,42 @@ +# Flow2Spec Doctor 诊断命令 + +## 定位 + +`flow2spec doctor` 是面向 Flow2Spec 使用者的只读项目体检入口,用于把分散的环境、初始化、协作和知识库检查汇总成一份可操作报告。首版不提供 `--fix`,不调用网络,也不修改任何项目文件。 + +## 命令契约 + +```bash +flow2spec doctor +flow2spec doctor --json +flow2spec doctor --help +``` + +- 普通输出逐项显示 `[PASS]`、`[WARN]`、`[FAIL]`,异常项附修复建议。 +- `--json` 输出 `ok`、包信息、当前目录、汇总计数和检查明细,适合脚本或 CI 消费。 +- 警告不阻塞,退出码为 `0`;存在错误时退出码为 `1`。 +- 未知参数直接报错并返回 `1`。 + +## 检查范围 + +1. 当前 Node.js 是否满足 `package.json` 的 `engines.node`。 +2. 项目根 `flow2spec.config.json` 是否存在且可解析。 +3. 根 `AGENTS.md` 与 `.Knowledge/manifest-routing.json` 是否存在。 +4. 已存在的 Agent 配置根是否完整:Codex 检查 `.codex/AGENTS.md`、`.codex/hooks.json`;Claude 检查 `.claude/settings.json`;Cursor 检查 `.cursor/hooks.json`。项目没有任何 Agent 配置根时只警告。 +5. 复用 `resolveDeveloperContext` 输出实际 `developerId`、解析来源和 `TASK_ROOT`;无身份回退 legacy `.task/` 时警告。 +6. 根 `.gitignore` 是否显式忽略 `.task/`。 +7. 复用 knowledge engine 做严格知识图校验,并检查 topic revision 与 topic frontmatter 到 routing 元数据的漂移。 + +## 实现边界 + +- CLI 分发和参数校验位于 `cli.js`,诊断逻辑与文本格式化位于 `lib/doctor.js`。 +- `lib/doctor.js` 允许测试注入 Node 版本、Git 身份与知识库检查,避免测试依赖开发机全局状态。 +- doctor 只读取项目根已存在的文件;修复动作由用户根据建议显式执行。 +- 旧 topic 缺 `revision` 属严格知识库错误,建议执行 `flow2spec kb build --fix-topics` 后再运行 `flow2spec kb check --strict`。 +- 首版不检查 npm registry 或全局旧包 `@double-codeing/flow2spec`,保持诊断快速、离线、确定。 + +## 文档入口 + +- 中文:`docs/命令说明.md` 的 `flow2spec doctor` 章节 +- English: `docs/en/commands-reference.md`, `flow2spec doctor` + diff --git a/.Knowledge/topics/flow2spec-doctor.md b/.Knowledge/topics/flow2spec-doctor.md new file mode 100644 index 0000000..10d0f60 --- /dev/null +++ b/.Knowledge/topics/flow2spec-doctor.md @@ -0,0 +1,28 @@ +--- +id: flow2spec-doctor +revision: 0 +summary: "只读检查 Flow2Spec 环境、初始化、协作上下文与知识库健康" +primary: feature +confidence: inferred +tags: [module] +--- +# Flow2Spec Doctor + +## 适用场景 + +用于回答或维护 `flow2spec doctor`、项目健康检查、环境诊断、初始化完整性、协作上下文和知识库严格校验。 + +## 已落地能力 + +- `flow2spec doctor` 输出人读报告;`flow2spec doctor --json` 输出稳定结构。 +- 检查 Node.js、项目配置、Agent 配置根、`developerId` / `TASK_ROOT`、`.task/` 忽略规则与知识图健康。 +- 告警返回 `0`,错误返回 `1`;全程只读、离线,不提供 `--fix`。 + +## 维护边界 + +- 命令分发:`cli.js` +- 诊断实现:`lib/doctor.js` +- 自动化测试:`scripts/test-doctor.js` +- 完整事实:`.Knowledge/stock-docs/Flow2Spec-doctor诊断命令.md` +- 用户文档:`docs/命令说明.md`、`docs/en/commands-reference.md` + diff --git a/cli.js b/cli.js index 2a879f0..c8a9905 100644 --- a/cli.js +++ b/cli.js @@ -15,6 +15,7 @@ const { normalizeLocale, } = require("./lib/flow2specConfig"); const knowledgeEngine = require("./lib/knowledgeEngine"); +const { runDoctor, formatDoctorReport } = require("./lib/doctor"); const { execFileSync } = require("child_process"); @@ -234,6 +235,7 @@ Flow2Spec - 统一知识库工作流(AI 配置入口) v${pkg.version} 用法: flow2spec init [agent ...] [--reset-knowledge] [--yes] [--locale zh-CN|en-US] 在当前项目初始化:写入 .Knowledge 与所选 agent 入口 flow2spec config 打印项目根 ${CONFIG_FILENAME} 的解析结果(缺省值合并后) + flow2spec doctor [--json] 只读检查运行环境、项目初始化、协作上下文与知识库健康 flow2spec kb 知识库协作引擎:status / check / plan / apply / build flow2spec version 显示当前 flow2spec 版本 flow2spec update 更新 flow2spec 到最新版本;更新后提示执行 f2s-kb-upgrade @@ -325,6 +327,32 @@ if (sub === "config") { process.exit(0); } +if (sub === "doctor") { + const doctorArgs = args.slice(1); + if (doctorArgs.includes("--help") || doctorArgs.includes("-h")) { + console.log(` +用法: + flow2spec doctor [--json] + +只读检查 Node.js、项目配置、Agent 初始化、协作上下文、.task 忽略规则与知识库健康。 +警告不阻塞(exit 0),错误会返回 exit 1;本命令不会修改文件或访问网络。 +`.trim()); + process.exit(0); + } + const unknown = doctorArgs.filter((arg) => arg !== "--json"); + if (unknown.length > 0) { + console.error(`doctor 不支持参数:${unknown.join(" ")}`); + process.exit(1); + } + const report = runDoctor(process.cwd()); + if (doctorArgs.includes("--json")) { + printJson(report); + } else { + console.log(formatDoctorReport(report)); + } + process.exit(report.ok ? 0 : 1); +} + if (sub === "kb") { const kbSub = args[1]; const kbFlags = new Set(args.slice(2).filter((arg) => String(arg || "").startsWith("--"))); diff --git a/docs/en/commands-reference.md b/docs/en/commands-reference.md index 840215f..05e197d 100644 --- a/docs/en/commands-reference.md +++ b/docs/en/commands-reference.md @@ -24,7 +24,7 @@ | `/f2s-kb-migrate` | One-time migration of a legacy knowledge base to `.Knowledge/` | KB Maintenance | | `/f2s-kb-upgrade` | Upgrade knowledge base template, align manifest + matchers shards | KB Maintenance | -The table above lists conversation-triggered `/f2s-*` skills. The repository also provides shell commands under `flow2spec kb status / check / plan / apply / build`; they merge a skill-produced `kb-delta.json` into the shared `.Knowledge/`. See [§ 7](#7-flow2spec-kb-cli-for-collaborative-merges). +The table above lists conversation-triggered `/f2s-*` skills. The repository also provides shell commands: `flow2spec kb status / check / plan / apply / build` handles collaborative knowledge merges (see [§ 7](#7-flow2spec-kb-cli-for-collaborative-merges)), while `flow2spec doctor` performs a read-only project health check (see [§ 8](#8-flow2spec-doctor-project-health-check)). --- @@ -725,7 +725,36 @@ See [Team Collaboration](./team-collaboration.md) for the operating model and [A --- -## 8) Quick Reference +## 8) `flow2spec doctor` (Project Health Check) + +`flow2spec doctor` provides one command for diagnosing why Flow2Spec is not behaving as expected. It is read-only, does not access the network, and never modifies project configuration or knowledge files. + +```bash +flow2spec doctor +flow2spec doctor --json +``` + +It checks: + +- whether Node.js satisfies the package's `engines.node` requirement +- whether `flow2spec.config.json`, root `AGENTS.md`, and `.Knowledge/manifest-routing.json` exist and are usable +- whether detected `.codex`, `.claude`, and `.cursor` roots contain their required entry and hook files +- the resolved `developerId`, source, and `TASK_ROOT`; falling back to the legacy `.task/` root produces a warning +- whether the root `.gitignore` ignores `.task/` +- strict knowledge graph health, topic revisions, and routing metadata drift + +Human-readable output marks each check with `[PASS]`, `[WARN]`, or `[FAIL]` and includes a repair suggestion when applicable. `--json` returns a stable `ok / package / cwd / summary / checks` structure for CI and scripts. + +Warnings alone return exit code `0`; any error returns `1`. Unknown flags such as `--fix` fail immediately. To repair legacy topics without a `revision`, follow the reported commands: + +```bash +flow2spec kb build --fix-topics +flow2spec kb check --strict +``` + +--- + +## 9) Quick Reference For typical work scenarios and full workflows, see [Usage Guide § 3. Typical Workflows](./usage-guide.md). diff --git "a/docs/\345\221\275\344\273\244\350\257\264\346\230\216.md" "b/docs/\345\221\275\344\273\244\350\257\264\346\230\216.md" index 4ab9cd9..07ef89a 100644 --- "a/docs/\345\221\275\344\273\244\350\257\264\346\230\216.md" +++ "b/docs/\345\221\275\344\273\244\350\257\264\346\230\216.md" @@ -24,7 +24,7 @@ | `/f2s-kb-migrate` | 旧版知识库一次性迁移到 `.Knowledge/` | 知识库维护 | | `/f2s-kb-upgrade` | 知识库模板升级,对齐 manifest + matchers 分片 | 知识库维护 | -上表是聊天里触发的 `/f2s-*` 技能。仓库还带一组 **shell CLI**:`flow2spec kb status / check / plan / apply / build`,用来把技能写好的 `kb-delta.json` 合入共享的 `.Knowledge/`,见 [§ 7) `flow2spec kb` CLI](#7-flow2spec-kb-cli协作合库)。 +上表是聊天里触发的 `/f2s-*` 技能。仓库还带 shell CLI:`flow2spec kb status / check / plan / apply / build` 用于协作合库,见 [§ 7](#7-flow2spec-kb-cli协作合库);`flow2spec doctor` 用于只读项目体检,见 [§ 8](#8-flow2spec-doctor项目体检)。 --- @@ -850,7 +850,36 @@ --- -## 8) 快速参考 +## 8) `flow2spec doctor`(项目体检) + +`flow2spec doctor` 在排查“为什么 Flow2Spec 没有按预期工作”时,一次检查本机运行环境和当前项目状态。命令只读、不联网,也不会自动修改配置或知识库。 + +```bash +flow2spec doctor +flow2spec doctor --json +``` + +检查范围: + +- Node.js 版本是否满足包的 `engines.node` +- `flow2spec.config.json`、根 `AGENTS.md` 与 `.Knowledge/manifest-routing.json` 是否存在且可用 +- 已存在的 `.codex`、`.claude`、`.cursor` 配置根是否包含必要入口和 hooks +- 实际解析出的 `developerId`、来源与 `TASK_ROOT`;无法识别身份而回退到 `.task/` 时给出警告 +- 根 `.gitignore` 是否忽略 `.task/` +- 知识图、topic revision 和 routing 元数据是否通过严格校验 + +文本输出用 `[PASS]`、`[WARN]`、`[FAIL]` 标记每项,并在异常项后给出修复建议。`--json` 输出稳定的 `ok / package / cwd / summary / checks` 结构,便于 CI 或脚本消费。 + +退出码规则:仅有警告时返回 `0`;任何错误返回 `1`。`--fix` 等未知参数会直接失败。需要修复旧 topic 缺少 `revision` 时,根据提示运行: + +```bash +flow2spec kb build --fix-topics +flow2spec kb check --strict +``` + +--- + +## 9) 快速参考 典型工作场景与完整链路见 [使用说明 § 三、典型工作场景](./使用说明.md)。 diff --git a/lib/doctor.js b/lib/doctor.js new file mode 100644 index 0000000..9433f07 --- /dev/null +++ b/lib/doctor.js @@ -0,0 +1,347 @@ +const fs = require("fs"); +const path = require("path"); + +const { AGENTS } = require("./agents"); +const { + loadFlow2specConfig, + CONFIG_FILENAME, +} = require("./flow2specConfig"); +const { resolveDeveloperContext } = require("./developerId"); +const knowledgeEngine = require("./knowledgeEngine"); + +const STATUS = { + pass: "pass", + warning: "warning", + error: "error", +}; + +function numericVersion(version) { + return String(version || "") + .replace(/^v/, "") + .split(".") + .slice(0, 3) + .map((part) => Number.parseInt(part, 10) || 0); +} + +function compareVersions(left, right) { + const a = numericVersion(left); + const b = numericVersion(right); + for (let index = 0; index < 3; index += 1) { + const difference = (a[index] || 0) - (b[index] || 0); + if (difference !== 0) return difference; + } + return 0; +} + +function satisfiesNodeEngine(version, engine) { + const minimum = String(engine || "").match(/>=\s*v?(\d+(?:\.\d+){0,2})/); + if (!minimum) return true; + return compareVersions(version, minimum[1]) >= 0; +} + +function makeCheck(id, label, status, message, repair = null, details) { + const check = { id, label, status, message, repair }; + if (details !== undefined) check.details = details; + return check; +} + +function checkKnowledge(cwd) { + try { + const graph = knowledgeEngine.loadKnowledgeGraph(cwd); + const validation = knowledgeEngine.validateKnowledgeGraph(graph, { + strictRevision: true, + }); + const normalized = knowledgeEngine.normalizeRoutingWithGraph(graph); + const routingDrift = + normalized.changed || + knowledgeEngine.stableStringify(normalized.routing) !== + knowledgeEngine.stableStringify(graph.routing); + const details = { + topicCount: validation.topicCount, + issues: validation.issues, + warnings: validation.warnings, + routingDrift, + }; + + if (!validation.ok || routingDrift) { + const reasons = [...validation.issues]; + if (routingDrift) reasons.push("routing metadata differs from topic frontmatter"); + return makeCheck( + "knowledge", + "知识库", + STATUS.error, + `知识图存在 ${reasons.length} 个问题。`, + "运行 flow2spec kb build --fix-topics,再运行 flow2spec kb check --strict。", + details, + ); + } + if (validation.warnings.length > 0) { + return makeCheck( + "knowledge", + "知识库", + STATUS.warning, + `知识图可用,但有 ${validation.warnings.length} 条警告。`, + "运行 flow2spec kb check --strict 查看详情。", + details, + ); + } + return makeCheck( + "knowledge", + "知识库", + STATUS.pass, + `${validation.topicCount} 个 topic 校验通过,routing 无漂移。`, + null, + details, + ); + } catch (error) { + return makeCheck( + "knowledge", + "知识库", + STATUS.error, + error.message || String(error), + "确认 .Knowledge/manifest-routing.json 与其引用的 topic、matcher 均存在且为有效格式。", + ); + } +} + +function isIgnoredByRootGitignore(cwd, entry) { + const gitignore = path.join(cwd, ".gitignore"); + if (!fs.existsSync(gitignore)) return false; + const lines = fs + .readFileSync(gitignore, "utf8") + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#")); + return lines.includes(entry) || lines.includes(entry.replace(/\/$/, "")); +} + +function runDoctor(cwd = process.cwd(), options = {}) { + const pkg = options.package || require("../package.json"); + const nodeVersion = options.nodeVersion || process.version; + const knowledgeCheck = options.knowledgeCheck || checkKnowledge; + const checks = []; + + const engine = pkg.engines?.node || ""; + const runtimeOk = satisfiesNodeEngine(nodeVersion, engine); + checks.push( + makeCheck( + "runtime", + "Node.js", + runtimeOk ? STATUS.pass : STATUS.error, + runtimeOk + ? `${nodeVersion} 满足 ${engine || "包要求"}。` + : `${nodeVersion} 不满足 ${engine}。`, + runtimeOk ? null : `升级 Node.js 到满足 ${engine} 的版本。`, + { version: nodeVersion, required: engine }, + ), + ); + + const configPath = path.join(cwd, CONFIG_FILENAME); + let config = null; + if (!fs.existsSync(configPath)) { + checks.push( + makeCheck( + "config", + "项目配置", + STATUS.error, + `缺少 ${CONFIG_FILENAME}。`, + "在项目根运行 flow2spec init。", + ), + ); + } else { + try { + config = loadFlow2specConfig(cwd); + checks.push( + makeCheck( + "config", + "项目配置", + STATUS.pass, + `${CONFIG_FILENAME} 存在且可解析。`, + null, + { locale: config.locale }, + ), + ); + } catch (error) { + checks.push( + makeCheck( + "config", + "项目配置", + STATUS.error, + error.message || String(error), + `修正 ${CONFIG_FILENAME} 的 JSON 格式。`, + ), + ); + } + } + + const agentsPath = path.join(cwd, "AGENTS.md"); + checks.push( + fs.existsSync(agentsPath) + ? makeCheck("agents-entry", "项目入口", STATUS.pass, "根 AGENTS.md 已就绪。") + : makeCheck( + "agents-entry", + "项目入口", + STATUS.error, + "缺少根 AGENTS.md。", + "运行 flow2spec init codex,或重新初始化所需 Agent。", + ), + ); + + const manifestPath = path.join(cwd, ".Knowledge", "manifest-routing.json"); + checks.push( + fs.existsSync(manifestPath) + ? makeCheck( + "knowledge-entry", + "知识库入口", + STATUS.pass, + ".Knowledge/manifest-routing.json 已就绪。", + ) + : makeCheck( + "knowledge-entry", + "知识库入口", + STATUS.error, + "缺少 .Knowledge/manifest-routing.json。", + "在项目根运行 flow2spec init。", + ), + ); + + const requiredAgentFiles = { + codex: ["AGENTS.md", "hooks.json"], + claude: ["settings.json"], + cursor: ["hooks.json"], + }; + const initializedAgents = Object.entries(AGENTS).filter(([, agent]) => + fs.existsSync(path.join(cwd, agent.root)), + ); + if (initializedAgents.length === 0) { + checks.push( + makeCheck( + "agent-roots", + "Agent 配置", + STATUS.warning, + "未检测到 .codex、.claude 或 .cursor 配置根。", + "运行 flow2spec init 初始化实际使用的 Agent。", + ), + ); + } else { + for (const [id, agent] of initializedAgents) { + const missing = (requiredAgentFiles[id] || []).filter( + (file) => !fs.existsSync(path.join(cwd, agent.root, file)), + ); + checks.push( + missing.length === 0 + ? makeCheck( + `agent-${id}`, + `${agent.label} 配置`, + STATUS.pass, + `${agent.root} 初始化文件完整。`, + ) + : makeCheck( + `agent-${id}`, + `${agent.label} 配置`, + STATUS.error, + `${agent.root} 缺少 ${missing.join("、")}。`, + `运行 flow2spec init ${id} 补齐配置。`, + { missing }, + ), + ); + } + } + + if (config) { + try { + const context = resolveDeveloperContext(config, { + cwd, + gitIdentity: options.gitIdentity, + skipGit: Boolean(options.gitIdentity), + }); + const warnings = [...context.warnings]; + if (context.legacy && context.enabled) { + warnings.push("未找到 developerId,将使用 legacy .task/ 根。"); + } + checks.push( + makeCheck( + "collaboration", + "协作上下文", + warnings.length > 0 ? STATUS.warning : STATUS.pass, + context.legacy + ? `使用 ${context.taskRoot}(${context.enabled ? "legacy" : "协作隔离已关闭"})。` + : `developerId=${context.developerId},TASK_ROOT=${context.taskRoot}。`, + warnings.length > 0 + ? "在 flow2spec.config.json 配置 collaboration.developerId。" + : null, + { ...context, warnings }, + ), + ); + } catch (error) { + checks.push( + makeCheck( + "collaboration", + "协作上下文", + STATUS.error, + error.message || String(error), + "修正 flow2spec.config.json 的 collaboration 配置。", + ), + ); + } + } + + const taskIgnored = isIgnoredByRootGitignore(cwd, ".task/"); + checks.push( + taskIgnored + ? makeCheck("task-ignore", "任务目录", STATUS.pass, ".task/ 已在根 .gitignore 中忽略。") + : makeCheck( + "task-ignore", + "任务目录", + STATUS.warning, + ".task/ 未在根 .gitignore 中忽略。", + "在根 .gitignore 中加入 .task/,或重新运行 flow2spec init。", + ), + ); + + checks.push(knowledgeCheck(cwd)); + + const summary = checks.reduce( + (result, check) => { + if (check.status === STATUS.pass) result.passed += 1; + if (check.status === STATUS.warning) result.warnings += 1; + if (check.status === STATUS.error) result.errors += 1; + return result; + }, + { passed: 0, warnings: 0, errors: 0 }, + ); + + return { + ok: summary.errors === 0, + package: { name: pkg.name, version: pkg.version }, + cwd: path.resolve(cwd), + summary, + checks, + }; +} + +function formatDoctorReport(report) { + const marker = { pass: "[PASS]", warning: "[WARN]", error: "[FAIL]" }; + const lines = [ + `Flow2Spec Doctor v${report.package.version}`, + `项目: ${report.cwd}`, + "", + ]; + for (const check of report.checks) { + lines.push(`${marker[check.status]} ${check.label}: ${check.message}`); + if (check.repair) lines.push(` 建议: ${check.repair}`); + } + lines.push( + "", + `结果: ${report.summary.passed} 通过,${report.summary.warnings} 警告,${report.summary.errors} 错误。`, + ); + return lines.join("\n"); +} + +module.exports = { + STATUS, + runDoctor, + formatDoctorReport, + satisfiesNodeEngine, + checkKnowledge, +}; diff --git a/package.json b/package.json index abafe27..a905b32 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@double-coding/flow2spec", - "version": "3.2.11", + "version": "3.2.12", "description": "在业务仓库初始化「文档驱动、可写回知识库」的 AI 协作骨架:项目根 .Knowledge 承载 stock-docs/req-docs 与机读路由,.cursor/.claude/.codex 写入 f2s-* 规则与技能(含 Karpathy 式编码行为准则,init 同步 rules / Codex topics / skills);init 只落结构与模板,业务内容由各 f2s-* 技能在对话中维护。", "homepage": "https://github.com/double-coding-lab/Flow2Spec#readme", "repository": { @@ -26,7 +26,7 @@ "registry": "https://registry.npmjs.org/" }, "scripts": { - "test": "node cli.js --help && node cli.js kb check && node scripts/test-knowledge-engine.js && node scripts/test-developer-id.js && node scripts/test-template-knowledge.js && node scripts/test-init-gitignore.js", + "test": "node cli.js --help && node cli.js kb check && node scripts/test-knowledge-engine.js && node scripts/test-developer-id.js && node scripts/test-template-knowledge.js && node scripts/test-init-gitignore.js && node scripts/test-doctor.js", "sync:agents": "node cli.js init cursor claude codex", "prepublishOnly": "node cli.js --help", "pack:check": "npm pack --dry-run", diff --git a/scripts/test-doctor.js b/scripts/test-doctor.js new file mode 100644 index 0000000..b541a58 --- /dev/null +++ b/scripts/test-doctor.js @@ -0,0 +1,108 @@ +const assert = require("assert"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { spawnSync } = require("child_process"); + +const { + runDoctor, + formatDoctorReport, + satisfiesNodeEngine, +} = require("../lib/doctor"); + +function write(file, content = "") { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, content, "utf8"); +} + +function createHealthyProject(root) { + write( + path.join(root, "flow2spec.config.json"), + `${JSON.stringify({ collaboration: { enabled: true, developerId: "alice" } })}\n`, + ); + write(path.join(root, "AGENTS.md"), "# Project\n"); + write(path.join(root, ".Knowledge", "manifest-routing.json"), "{}\n"); + write(path.join(root, ".codex", "AGENTS.md"), "See ../AGENTS.md\n"); + write(path.join(root, ".codex", "hooks.json"), "{}\n"); + write(path.join(root, ".gitignore"), "node_modules/\n.task/\n"); +} + +const passKnowledge = () => ({ + id: "knowledge", + label: "知识库", + status: "pass", + message: "fixture healthy", + repair: null, +}); + +assert.strictEqual(satisfiesNodeEngine("v16.0.0", ">=16"), true); +assert.strictEqual(satisfiesNodeEngine("v15.9.0", ">=16"), false); +assert.strictEqual(satisfiesNodeEngine("v20.11.1", ">=16"), true); + +const healthyRoot = fs.mkdtempSync(path.join(os.tmpdir(), "flow2spec-doctor-ok-")); +const warningRoot = fs.mkdtempSync(path.join(os.tmpdir(), "flow2spec-doctor-warn-")); +const errorRoot = fs.mkdtempSync(path.join(os.tmpdir(), "flow2spec-doctor-error-")); + +try { + createHealthyProject(healthyRoot); + const healthy = runDoctor(healthyRoot, { + nodeVersion: "v20.11.1", + gitIdentity: { email: null, name: null }, + knowledgeCheck: passKnowledge, + }); + assert.strictEqual(healthy.ok, true); + assert.strictEqual(healthy.summary.errors, 0); + assert.strictEqual(healthy.summary.warnings, 0); + assert.match(formatDoctorReport(healthy), /\[PASS\] Node\.js/); + assert.match(formatDoctorReport(healthy), /developerId=alice/); + + write( + path.join(warningRoot, "flow2spec.config.json"), + `${JSON.stringify({ collaboration: { enabled: true, developerId: "" } })}\n`, + ); + write(path.join(warningRoot, "AGENTS.md"), "# Project\n"); + write(path.join(warningRoot, ".Knowledge", "manifest-routing.json"), "{}\n"); + const warning = runDoctor(warningRoot, { + nodeVersion: "v20.11.1", + gitIdentity: { email: null, name: null }, + knowledgeCheck: passKnowledge, + }); + assert.strictEqual(warning.ok, true, "warnings must not block doctor"); + assert.ok(warning.summary.warnings >= 2, "missing agent roots and legacy context warn"); + + write(path.join(errorRoot, "flow2spec.config.json"), "{ invalid json\n"); + const failed = runDoctor(errorRoot, { + nodeVersion: "v14.0.0", + knowledgeCheck: () => ({ + id: "knowledge", + label: "知识库", + status: "error", + message: "fixture broken", + repair: "repair fixture", + }), + }); + assert.strictEqual(failed.ok, false); + assert.ok(failed.summary.errors >= 4); + assert.ok(failed.checks.some((check) => check.id === "runtime" && check.status === "error")); + + const cliPath = path.resolve(__dirname, "..", "cli.js"); + const unknownFlag = spawnSync(process.execPath, [cliPath, "doctor", "--fix"], { + cwd: healthyRoot, + encoding: "utf8", + }); + assert.strictEqual(unknownFlag.status, 1); + assert.match(unknownFlag.stderr, /doctor 不支持参数/); + + const help = spawnSync(process.execPath, [cliPath, "doctor", "--help"], { + cwd: healthyRoot, + encoding: "utf8", + }); + assert.strictEqual(help.status, 0); + assert.match(help.stdout, /不会修改文件或访问网络/); + + console.log("doctor tests passed"); +} finally { + fs.rmSync(healthyRoot, { recursive: true, force: true }); + fs.rmSync(warningRoot, { recursive: true, force: true }); + fs.rmSync(errorRoot, { recursive: true, force: true }); +}