Impeccable doctor 深度解析:报告并修复设计工件与安装版本之间的漂移
本文围绕 Impeccable 技能中的 doctor 参考文档(.agents/skills/impeccable/reference/doctor.md)展开,系统讲解 doctor 命令如何报告并修复项目内 Impeccable 工件(PRODUCT.md、DESIGN.md 及其 sidecar、.impeccable/config.json、持久化的 surface brief、设计 hook)与当前安装版本所读取内容之间的漂移。读完本文,你可以掌握三类漂移的区分方法、severity 三级处置协议、monorepo 场景下的关键 findings 语义,以及 --fix 自动化迁移的源码级边界,从而在一次 doctor 运行中做出正确且克制的问题处置决策。
定位:这是维护,不是设计
doctor 文档开宗明义地界定了这条命令的性质:
Report and repair drift between this project's Impeccable artifacts and what the installed version reads: PRODUCT.md, DESIGN.md and its
.impeccable/design.jsonsidecar,.impeccable/config.json, persisted surface briefs, and the design hook.This is maintenance, not design. Do not redesign anything, do not open files outside the one the report names, and do not run any other command as a side effect.
也就是说,doctor 的巡检对象严格限定为五类工件:
- PRODUCT.md —— 产品记录(含 schema 戳记与 Platform 等章节);
- DESIGN.md 及其
.impeccable/design.jsonsidecar —— 设计系统文档与机器可读附属文件; .impeccable/config.json(及 gitignored 的.impeccable/config.local.json)—— 项目配置;- 持久化的 surface briefs —— 各 UI surface 的持久化 brief;
- 设计 hook 的安装状态。
同时文档划出了操作红线:不做任何重新设计、不打开报告未点名的文件、不把其他命令当作副作用执行。这一红线在 doctor.mjs 的实现中得到严格贯彻:--fix 只执行被标记为 severity auto 且确实实现了机械迁移的动作(移动 sidecar、为 PRODUCT.md 打 schema 戳),其余一切需要用户判断的项只报告、不修改,且退出码在非运行本身失败时恒为 0——findings 不是错误。
三类漂移:先分清“过期”到底是什么
文档最重要的概念贡献,是把笼统的“out of date”拆成三种性质完全不同的漂移,并要求处置它们时保持分离:
| 漂移类型 | 含义 | 归属 |
|---|---|---|
| Tool version(工具版本漂移) | 已安装技能比发布版本旧。context.mjs 在会话启动时以 UPDATE_AVAILABLE 报告,npx impeccable update 可修复 |
不是 doctor 的职责 |
| Schema drift(模式漂移) | 工件由旧版 Impeccable 写入:存在无人读取的字段、缺失如今期望的字段、文件位于已退役的位置。机械可判定,doctor 能修复其中大部分 | doctor 的核心职责 |
| Truth drift(事实漂移) | 代码已经演进,文档不再描述现状。没有任何文件对比能裁定此事 | 归 document(DESIGN.md)与 init(PRODUCT.md)所有,doctor 只负责把“具体的差距”交过去,而非模糊的怀疑 |
这一三分法在 lib/staleness.mjs 的模块头部注释中被完整复述,并且落实到两个成本层级:
- Tier 1(boot 层):
collectBootFindings只花“启动本来就要花”的成本——解析已在内存中的 markdown、对有限路径做 stat、读取启动时本就要读的两个小 JSON。不遍历目录、不动 git、不跨 workspace 扫描; - Tier 2(doctor 层):按需运行,可以遍历目录、shell 调用 git、把声明的 token 与真实 CSS 对比。git 漂移、跨 workspace 扫描、ignore 列表对规则注册表的校验都在这层。
Findings 被设计为数据而非散文(每条含 id、artifact、path、severity、summary、fix),因此两层与 JSON 输出渲染的是同一集合。
Step 1:运行巡检
文档给出的基础命令:
node .agents/skills/impeccable/scripts/doctor.mjs --json
完整参数(来自 doctor.mjs 的 usage 文本):
node doctor.mjs [--json] [--fix] [--target <path>]
--json:以 JSON 输出 findings,供技能命令消费;--fix:仅应用机械迁移(severityauto);--target <path>:在 monorepo 中选定一个 workspace。文档特别警告:当用户在 monorepo 里点名了某个 workspace、文件或路由时必须加此参数,否则报告描述的是仓库根目录——“在 monorepo 中那往往是错误的项目”。参数解析走 lib/target-args.mjs 的parseTargetOptions,且以严格模式执行。
JSON 输出的顶层字段(doctor.mjs):projectRoot、repoRoot、isMonorepo、productPath、designPath、platform、ruleRegistryAvailable、findings、workspaces,执行 --fix 时另有 fixes。
文档对输出有三个精确的解读要求:
- 每条 finding 的结构是
id、artifact、path、severity、summary、fix;monorepo 场景下额外有workspaces,列出各 app 的 product / design 解析状态; ruleRegistryAvailable: false意味着被 ignore 的规则 id 未能对规则注册表做校验。此时必须如实说明这一点,而不能暗示 ignore 列表是干净的。源码中,loadKnownRuleIds 依次尝试技能自带的detector/detect-antipatterns.mjs与源码仓库引擎cli/engine/detect-antipatterns.mjs两处,解析不到则返回null,忽略规则校验随之跳过而非把所有 id 视为未知;findings为空数组是好的结果:一句话说清楚即可,然后停止。
Step 2:按 severity 行动,而不是按“严重度”行动
文档强调:severity 表达的是“应该发生什么”,而不是“有多糟”。三种取值及处置协议:
auto —— 不含决策,直接执行
只运行一次:
node .agents/skills/impeccable/scripts/doctor.mjs --fix
然后用一行报告它移动/变更了什么。不要事先征求许可,事后也不要再逐条询问。applyFixes(doctor.mjs)的实际行为边界值得注意:
design-sidecar-legacy-path:把 sidecar 从退役位置移动到规范位置.impeccable/design.json(候选位置列表见 designSidecarCandidatesFor,canonical 在前,DESIGN.json等旧位置在后)。若规范位置已存在则跳过、不覆盖;legacy-live-state:只报告、永不自动删除。原因是运行中的 live 会话仍读取这些旧文件,doctor 运行弄丢会话状态比留一个陈旧文件后果更糟。报告会说明“何时可手动删除”;- PRODUCT.md 的 schema 戳(
<!-- impeccable:product-schema N -->):在--fix中作为纯附加安全动作直接补打,注释解释其价值——阻止后续版本反复提出用户已经答过一遍的访谈。
其余 auto 项若没有实现自动迁移,会被列入 skipped 并附原因;所有非 auto 项一律以 needs a decision from the user 跳过。
mention —— 让用户知道,但此刻不需要决策
逐条用一句话陈述,并附上它给出的修复建议。
route —— 需要特定命令
点名命令与它要弥合的差距,仅当用户在本轮要求时才执行。文档特别指出:init 和 document 是对话,不是可以无人值守执行的修复。
三类在一次运行中报告完毕。文本报告按 route → mention → auto 的顺序分组渲染(renderText),未执行 --fix 但存在 auto 项时会在末尾提示运行 --fix 或走完整的 doctor 流程。
Step 3:废弃字段是约束性的(binding)
文档给出了一个反直觉但关键的规则:报告废弃字段的 finding 不是风格建议。一旦 finding 报告了废弃字段(当前为 ## Register),从此以后对所有决策都要把该字段当作不存在,无论它持有什么值,并主动提出删除该节。“保留以防万一”恰恰是退役轴心继续左右当前输出的方式。
这一规则有源码支撑。lib/artifact-schema.mjs 中的 PRODUCT_DEPRECATED_SECTIONS 不仅给出被废弃标题,还给出理由:v4 用四个 visitor mode(Persuade、Operate、Read、Experience)替代了 brand/product register 轴心,四个 mode 按 surface 选择并持久化在该 surface 的 brief 中,如今没有任何代码读取 ## Register。注释明确解释了为什么要告诉 agent 理由——“只被告知字段被废弃时,agent 倾向于‘以防万一’地保留它,这正是 v3 的 register 值继续左右 v4 输出的路径”。
检测入口在 checkProduct:对 PRODUCT.md 做纯函数式的节标题匹配,生成 product-deprecated-register(mention 级),其 fix 字段就是文档 Step 3 那段处置话术本身。
同一函数还负责两种模式级 finding:
product-schema-legacy(route):PRODUCT.md 既无 schema 戳,也缺少当前记录新增的 v4 章节(Positioning、Operating Context、Evidence on Hand、Product Principles),说明它早于当前记录。修复是提供init——保留已确认的答案、以访谈补齐缺口,不要凭推断重写文件;product-schema-outdated(route):戳记版本低于当前版本(当前PRODUCT_SCHEMA_VERSION = 1),同样路由到init。
采用 schema 版本而非技能版本号的原因在 artifact-schema.mjs 头部注释中写明:v4.0.0 写的 PRODUCT.md 在 v4.0.1 下并不陈旧,若戳记发布版本号则每个 patch 都会让所有工件“变旧”;schema 版本只在形状变化时变化——恰好是欠下迁移的时刻。
Step 4:不要对 truth drift 过度断言
这是文档中最体现工程克制的一条。design-md-drift 统计的是 DESIGN.md 最后一次编辑之后,视觉源码目录收到的 commit 数。文档的告诫是:commit 数不是矛盾(contradiction)。要报告这个数字、说明它度量的是什么;如果用户想知道文档是否真的错了,就把 DESIGN.md 与当前 token、组件对照阅读后作答。永远不要因为数字大就断言 DESIGN.md 已经陈旧。
实现层面(checkDesignDrift):
- 监控目录固定为
src、app、pages、components、site、styles、public(仅统计存在的); - 只在 git 仓库内、DESIGN.md 已被 git 跟踪、且存在可定位的最后一次 DESIGN.md 提交时才运行;
- 阈值默认为 25 个 commit,低于阈值静默——“小到属于常规维护”的量不值得打扰;
- finding 的
summary文本本身就写着 “This counts commits, not contradictions: it says the document is worth re-reading, not that it is wrong.”,fix则是“在把它当权威之前先对照当前 token 与组件阅读 DESIGN.md;若确实漂移,document可从代码重新生成”。
同样的克制适用于 workspace-context-inherited:workspace 继承仓库根的 PRODUCT.md 是设计内的行为,而非缺陷;“一份产品记录是否如实描述了多个 app”是用户的问题,不是检查器能回答的缺陷。对应实现 checkWorkspaces 把它生成为 mention 级 finding,措辞是 “Inheritance is intended; whether one record truthfully describes these apps is not something this check can tell.”
Monorepo 关键 findings
文档“Monorepo notes”一节列出了四个 monorepo 专属要点,逐一展开:
workspace-platform-native-evidence:最重要的一条
一个 workspace 携带原生构建文件,却继承了解析为 web 的根 PRODUCT.md——它将终身获得 web 指导,且永远不加载 ios.md 或 android.md 参考。修复是在该 workspace 下放一份子 PRODUCT.md,因为一份继承记录无法同时承载两个平台。
底层判定在 checkNativePlatformEvidence,证据集是有限且廉价的:
- 文件证据:
pubspec.yaml(Flutter → adaptive)、ios/Podfile(ios)、android/build.gradle与android/build.gradle.kts(android)、ios/Runner.xcodeproj(ios); - 依赖证据:
package.json的 dependencies/devDependencies 中出现react-native、expo、@react-native/metro-config(均记为 adaptive)。
只有当项目解析结果为 web(显式 ## Platform: web、无 Platform 节、或根本没有 PRODUCT.md)时才检查;平台建议值在证据跨平台或含 adaptive 时取 adaptive,否则取证据平台。monorepo 扫描版还会区分该 workspace 是“继承根 PRODUCT.md”还是“有自己的 PRODUCT.md”,并据此给出不同的修复话术。
config-project-roots-match-nothing:glob 全部落空
.impeccable/config.json 的 projectRoots 中所有正向 glob 都没有匹配到任何目录,于是仓库根静默顶替为“活动项目”,且没有别的信号会亮起来。常见原因是 workspace 目录改名。处置:报告这些 pattern,并询问它们本应指向哪些目录。实现(checkProjectRoots)只在“有正向 pattern 且候选列表为空”时触发,且复用 boot 已完成的目录遍历,不重复付出成本。
config-invalid-build-path 与 config-build-path-unset:同一个键 buildPath
buildPath 位于 .impeccable/config.json(或 gitignored 的 .impeccable/config.local.json,后者对应当开发者生效),取值只有 comp 与 code(staleness.mjs 中 BUILD_PATH_VALUES),决定新 surface 是从生成的 comp 起步还是直接在代码中构建。两个 finding 的语义要点:
- 读不到的值不会回退到“另一条路径”。
config-invalid-build-path的fix字段原文:“An unreadbuildPathdoes not fall back to the other path; it falls back to the default, so a project meaningcodehas been building comp-led.”——一个本意是code的项目可能一直在走 comp 路线,报告时必须给出确切值; config-build-path-unset只在该项目做过方向性工作且从未记录偏好时触发,证据是.impeccable/surfaces或.impeccable/mocks/decision目录存在(两个 stat,Tier 1 负担得起);且该 offer 只应出现在工具面存在图像生成能力时——没有图像生成就没有可选项,也无需多言。文档原文:“Without image generation there is nothing to choose and nothing to say.”
workspaces 表:先展示,再提议
在提出任何变更前,先用 workspaces 表向用户展示哪些 app 自带上下文、哪些继承、哪些什么都没有。表中每条 workspace 含 name、path、productStatus、productPath、designStatus、designPath、platform(describeWorkspaceContext 与 checkWorkspaces),文本报告以 product: ... design: ... platform: ... 形式逐行渲染。
完整 findings 速查表
结合 staleness.mjs(Tier 1)与 staleness-deep.mjs(Tier 2),doctor 报告可能出现的全部 finding id 如下:
| finding id | 工件 | severity | 触发条件 | 修复去向 |
|---|---|---|---|---|
product-deprecated-register |
PRODUCT.md | mention | 仍带 ## Register 节 |
视为不存在,提议删除 |
product-schema-legacy |
PRODUCT.md | route | 无 schema 戳且缺 v4 章节 | init |
product-schema-outdated |
PRODUCT.md | route | 戳记版本低于当前版本 | init |
platform-native-evidence |
PRODUCT.md | mention | 解析为 web 但携带原生构建证据 | 询问 ## Platform 取值 |
design-sidecar-legacy-path |
design.json | auto | sidecar 位于退役位置 | --fix 移至 .impeccable/design.json |
design-sidecar-schema-outdated |
design.json | route | schemaVersion 缺失或低于 2 |
document 重新生成 |
design-sidecar-stale |
design.json | mention | DESIGN.md 的 mtime 晚于 sidecar | document 刷新 sidecar |
design-md-drift |
DESIGN.md | route | 视觉源码目录 ≥25 个新 commit | 对照阅读;确需更新则 document |
design-md-coverage |
DESIGN.md | mention | 规范章节(seed 文档:colors/typography;完整文档另加 components)为空 | 询问从未适用还是从未撰写 |
config-unknown-keys |
config.json | mention | 顶层存在无人读取的键 | 报告确切键名 |
config-invalid-build-path |
config.json | mention | buildPath 非 comp/code |
报告确切值,不回退假设 |
config-unknown-detector-keys |
config.json | mention | detector 子树存在未知键(如 ignoreRule 拼成单数) |
报告确切键名 |
config-build-path-unset |
config.json | mention | 有方向工作证据但从未记录 buildPath |
有图像生成时一次性询问 |
config-project-roots-match-nothing |
config.json | mention | projectRoots 全部 glob 未命中 |
询问 pattern 应指向的目录 |
surface-brief-orphaned |
surface brief | mention | brief 的 primaryTarget 文件已不存在(URL 与 route: 目标跳过) |
询问是改指还是删除 brief |
detector-ignore-rules-unknown |
config.json | mention | ignoreRules 含注册表中不存在的规则 id |
删除安全;保留死 ignore 会掩盖规则已消失 |
detector-ignore-files-missing |
config.json | mention | ignoreFiles 含已不存在的非 glob 路径 |
询问文件是移动还是删除 |
hook-script-missing |
hook manifest | mention | 已安装 hook 但脚本路径解析后不存在 | impeccable hooks on 重装 |
hook-enabled-conflict |
config.json | mention | manifest 已装 hook 但 hook.enabled: false |
询问启用还是卸载 |
legacy-live-state |
live state | auto | 存在 .impeccable-live.json / .impeccable-live |
报告;无 live 会话时手动删除 |
workspace-platform-native-evidence |
PRODUCT.md | mention | workspace 有原生构建文件但记录解析为 web | 在该 workspace 写子 PRODUCT.md |
workspace-context-inherited |
PRODUCT.md | mention | 有 workspace 继承根 PRODUCT.md | 询问记录是否如实描述各 app |
几个实现细节值得留意:
- hook 检查的占位符策略(checkHookInstallation):doctor 会按 provider(claude-code、codex、cursor、github、grok 等,见
HOOK_MANIFESTS_BY_PROVIDER)定位 hook manifest,从命令行中提取脚本路径 token。${CLAUDE_PROJECT_DIR}按项目根展开(历史上不展开曾导致假阳性);${CLAUDE_PLUGIN_ROOT}、${PLUGIN_ROOT}、${GROK_PLUGIN_ROOT}及$(...)命令替换则无法静态解析,一律跳过——注释的原则是“doctor 从不断言它无法验证的否定”; - ignore 校验对注册表不可用的处理:
loadKnownRuleIds返回null表示“无法检查”,忽略规则校验整体跳过,而非把所有 id 判为未知——这正是ruleRegistryAvailable: false语义的出处; - DESIGN.md 为何不带 schema 戳:它遵循外部 design.md 规范(由 Stitch 的 linter 校验),额外的 frontmatter 键有 lint 失败风险,而其全部陈旧性信号(sidecar schema 版本、sidecar mtime、章节覆盖、git 漂移)都可无需戳记地测量(见 artifact-schema.mjs 头部注释)。
与 boot 检查的关系,以及如何退出
文档最后一节说明:context.mjs 在每次会话启动时报告这些 finding 的廉价子集(Tier 1),并按项目节流为每周一次。退出方式:
- 在
.impeccable/config.json中设置"stalenessCheck": false永久静默; - 设置环境变量
IMPECCABLE_NO_STALENESS_CHECK=1仅对单次会话静默; - doctor 命令本身在检查关闭后依然可用——文档明确建议的组合是:想要“只在主动要报告时才看”的用户,就关闭 boot 检查、保留 doctor 按需运行。
源码侧的对应关系:context.mjs 的 appendStalenessDirective 先查 stalenessCheckDisabled(读环境变量与 config.json/config.local.json 的 stalenessCheck 布尔值,local 覆盖 shared),再跑 collectBootFindings,且包在 try/catch 里——陈旧性检查永远不允许成为会话启动失败的原因。节流逻辑在 lib/staleness-notice.mjs:RENOTIFY_INTERVAL_MS 为 7 天,状态存放在用户主目录(默认 ~/.impeccable/staleness-check.json,可用 IMPECCABLE_STALENESS_CACHE 重定向)而非项目内,因此 clone 不会继承他人的“已忽略”标记;mention/route finding 在窗口期内不重复上报,而 auto finding 不受节流、不对用户展示——它们是“下次写入本就会执行的迁移”,agent 每个会话都需要看到,用户则永远不需要。
验证与测试入口
doctor 的行为契约由 tests/doctor.test.mjs 覆盖,可作行为核对清单:
checkDesignDrift:非 git 仓库静默、阈值以下静默、超过阈值后“以代理(proxy)身份”报告 commit 数、只统计最后一次 DESIGN.md 编辑之后的 commit、未跟踪的 DESIGN.md 静默;checkDesignCoverage:点名空的规范章节、seed 文档豁免 components 但仍要求 colors/typography、frontmatter 值判定规则(空映射、布尔、空集合字面量不计为覆盖);checkDetectorIgnores:未知规则 id 被标记、注册表不可用时整体跳过、通配符*被接受、glob 形式的ignoreFiles不做存在性断言;checkHookInstallation:覆盖裸路径、bundle 相对路径、${CLAUDE_PROJECT_DIR}展开、#399守护式形式、#476单引号绝对形式、GitHub$(git rev-parse --show-toplevel)形式永不报 missing、plugin-root 占位符永不报 missing;checkWorkspaces:继承 web 记录的原生 workspace 被标记、自声明原生平台的 workspace 不标记、继承被报告为信息而非缺陷。
小结
doctor 是 Impeccable 的“漂移巡检器”:以一条 node .agents/skills/impeccable/scripts/doctor.mjs --json [--target <path>] 输出结构化 findings,用 --fix 完成不含判断的机械迁移(sidecar 归位、PRODUCT.md 打戳),把需要决策的事项按 mention/route 分级上交,并把 truth drift 严格限定为“值得重读的信号”而非“文档已错的断言”。理解它的关键是三件事:区分工具版本、模式漂移、事实漂移三种“过期”;severity 表达该发生什么而非多糟;所有检查器宁可报告代理指标并标注其身份,也不做无法验证的断言。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00