# .tianshu-mcp/acceptance.json 规范 验收配置分**三级继承**(issue #20):全局兜底 → 项目级覆盖 → 任务级临时覆盖。 一个天枢宿主下挂载多个同类项目时,把公共策略写进全局层即可,不必逐项目建文件。 ## 配置优先级 ### 第一段:三级继承(同名字段,后一层覆盖前一层) | 顺序 | 层 | 文件 / 来源 | 缺失时 | |---|---|---|---| | 1(最低) | **全局兜底** | `<数据目录>/acceptance.default.json`(数据目录默认 `~/.tianshu-mcp`,可用环境变量 `TIANSHU_MCP_HOME` 覆盖) | 视为空配置,**不报错** | | 2 | **项目级覆盖** | `/.tianshu-mcp/acceptance.json` | 视为空配置 | | 3(最高) | **任务级临时覆盖** | `run_task` / `verify_task` 的 `acceptanceOverride` 参数 | 不传即无此层 | 三层的合并结果再落到下面第二段(检查集来源)。要点: - **合并粒度是「字段」**:高优先级层**显式书写**了某字段就整体取胜,未书写的字段沿用下层。 - **数组整体覆盖,不拼接**:`checks` 是高优先级层写了就整段替换。拼接会让「项目追加一项检查」 变成「项目无法移除全局检查」,歧义大且不可预测。 - **`visual` 同样整体覆盖,不做跨层深合并**。理由:`visual` 的 schema 几乎每个字段都带默认值, 一旦深合并,低优先级层的**显式**取值会被高优先级层「未书写、仅因默认值而出现」的字段静默覆盖 —— 这正是 `requireChanges` 曾经踩过的 `.default(true)` 污染。需要跨层复用视觉配置时,请在项目层 写完整段 `visual`。 - **坏文件 fail-closed**:某一层「存在但读不了 / JSON 写坏 / 字段不合法」**不会**被当成空配置静默跳过, 而是让该轮验收进 `needs_attention`,消息里指明是哪一层、哪个文件(与项目级既有语义一致)。 ### 第二段:基础检查集的来源(上一段合并结果内) 1. 合并结果的 `checks`(若三层都没写,则往下) 2. server 数据目录 `projects.json[pathHash].verify`(管理员补录) 3. 默认集(按项目技术栈自动推导,见下) ### 第三段:`extraChecks`(最高,独立于三级继承) `verify_task` 调用时的 `extraChecks` **追加**到基础集之后;`checksMode:"replace"` 才替换为基础集不加载。 ## 查看最终生效配置 ```bash # 只给项目路径:看全局层 + 项目层 tianshu-mcp config acceptance # 再叠加某任务的临时覆盖(读该任务快照里的 acceptanceOverride) tianshu-mcp config acceptance --task ``` 输出 JSON 含:各层是否存在的 `layers`、实际参与合并的 `appliedOrder`(低 → 高)、 最终取值 `effective` 与一行可读摘要 `summary`。每轮验收也会往 `server.log` 写一行同源摘要。 该命令在创建 MCP server **之前**返回,只写 stdout 面向人,不触碰协议流。 ## 文件格式 三层用的是**同一份格式**(全局层文件名为 `acceptance.default.json`,项目层为 `acceptance.json`): ```jsonc { // 默认 true:git 项目相对动工前基线零变更时验收失败 "requireChanges": true, // 选填:命令检查并行度(整数 1-4,越界值会被 clamp 到 1-4 而不是整份配置失效)。 // 1=串行(与历史行为一致);缺省继承 server 数据目录 config.json 的 verifyConcurrency(默认 2) "verifyConcurrency": 2, // checks 数组:每条 = 一项自动命令检查 "checks": [ { "name": "typecheck", // 必填,报告中显示名 "cmd": ["npm", "run", "typecheck"], // 必填:argv 数组(**推荐一律用这种形态**,非 shell) // 兼容:也可以写字符串,会被极简分词(不经过 shell)。但这有几条**必须知道的坑**: // "cmd": "npm run typecheck" "timeoutMs": 120000, // 选填,单条超时;缺省 server 级 5 分钟 "optional": false // 选填;optional:true 失败只记 warning,不影响本轮 verdict }, { "name": "lint", "cmd": ["npm", "run", "lint"] }, { "name": "test", "cmd": ["npm", "test"] } ] } ``` ## 语义 - 每条命令在**项目根目录**、以结构化 argv 执行(`shell:false`,不拼接 shell 字符串),stdout/stderr 写入该轮 `verify-N.log`,报告附输出尾部。 - **有界并行**:命令检查按 `verifyConcurrency` 并行执行(worker 池,上限 4)。报告中各 check 的展示顺序恒为声明顺序(与完成顺序无关);并行时每条 check 先写独立临时日志,全部结束后按声明顺序拼成同一份 `verify-N.log`(文件名与格式和串行完全一致)。`verifyConcurrency:1` 退化为逐条串行。任一 check 原有 `timeoutMs`/取消语义不变。 ### ⚠ `cmd` 请一律用数组形态(字符串形态的坑) **推荐写法永远是数组**:`"cmd": ["npm", "run", "typecheck"]`——每个元素就是一个 argv,零歧义。 字符串形态仅为**兼容保留**,它走的是极简分词(`src/config/store.ts` 的 `splitCmd()`),只识别整段引号包裹,**没有转义支持**。以下行为都是实测事实: | 你写的字符串 | 实际解析结果 | 说明 | |---|---|---| | `npm run "a b"` | `["npm","run","a b"]` | 引号包裹生效(正确用法) | | `node "a\"b"` | `["node","a\\","b\""]` | **转义无效**:`\` 是普通字符,`\"` 逃不出引号 | | `npm run "unclosed arg` | `["npm","run","\"unclosed","arg"]` | **引号不闭合不报错**,引号退化为普通字符 | | `cmd "" x` | `["cmd","","x"]` | 空引号产出空参数 | **后果**:含空格的路径(如 `C:\Program Files\...`)必须**手写整段引号**,写错(忘引号、引号不闭合、想用 `\"` 转义)**不会报错**,而是**静默拆成多个 argv**,命令可能以错误参数执行。凡是路径/参数可能含空格、或需要转义,请用数组形态。这些语义由 `test/unit/core.test.ts` 的边界用例锁定。 ### ⚠ 并行的脚本干扰 `verifyConcurrency` 默认 2(**默认值由串行变为 2**)。并行执行的 checks 共享同一个项目工作区,以下形态的脚本互相干扰时可能产生误失败或误通过,**建议显式设 `"verifyConcurrency": 1`**(或拆轮验收): - 写构建产物目录的 checks(如两个 check 都触发 `build`/覆盖同一 `dist/`); - 带 `--fix` / 自动改写源码的 checks(如 `eslint --fix`、`prettier --write` 与 `tsc` 并行——一边改一边编译); - 共享缓存目录的 checks(如同一 package manager 缓存、同一 `.pytest_cache`/覆盖率文件)。 - **任一非 optional 检查失败 ⇒ 该轮验收失败**;跳过/超时各自标记。 - **零用例 fail-closed**:非 optional 测试命令即使退出码为 0,只要输出明确表示未执行任何测试,仍判失败。 - **零变更 fail-closed**:Git 项目在 `requireChanges:true`(默认)时,若相对动工前基线没有已跟踪、未跟踪或 diffstat 变更,新增 `no-changes` 失败项。纯问答/分析任务可显式设置 `"requireChanges": false`;非 Git 项目跳过该门禁并在报告中注明。 - 内置额外检查(不经配置): - `git-diff-check`:`git diff --check`(空白错误);非 git 仓库自动跳过。 - 若配置缺失/解析失败,自动落回更低优先级来源,最终为空则只有内置检查,并在报告注明。 ## 默认集(无任何配置时自动推导,v1 覆盖) | 检测到 | 检查 | 缺失处理 | |---|---|---| | 任意项目 | `git diff --check`(内置) | 非 git 仓库跳过并标注 | | `package.json` 有 scripts | `npm run typecheck` / `lint` / `test` / `build`(各自独立) | 脚本不存在即跳过并标注 | | `tsconfig.json` 且无 npm scripts | `npx tsc --noEmit` | — | | `pytest.ini` | `pytest -q` | — | | `go.mod` | `go test ./...` | — | | `Cargo.toml` | `cargo test` | — | > 规则数据化,可后续扩展;不需要改代码。 ## 检查语义(R4 定稿) - **optional:true**:该检查失败只记为 warning(`report.message` 标注 "optional 检查未通过"),**不影响本轮 verdict**;必选(默认)失败才使 verdict=failed。 - **extraChecks 追加**:默认 `checksMode:"append"`——先解析项目/默认检查,再**追加** extraChecks(不削弱基础门禁)。`checksMode:"replace"` 才完全替换为只跑 extraChecks。 - **`acceptanceOverride`(任务级临时覆盖)**:`run_task` / `verify_task` 的可选参数,格式与 `acceptance.json` 相同。 它**只对当次任务/当次验收生效**,并随任务快照保存 —— 这是**任务数据**而非配置文件: 不写入任何 `acceptance*.json`、不影响同项目的其他任务、也不影响其他项目。 同一任务的后续 `rework_task` / `continue_task` 沿用该快照,因此覆盖继续生效。 无项目模式(`projectPath` 省略)**显式拒绝**该参数(没有项目验收可覆盖,静默忽略会误导调用方)。 注意:该参数计入 `idempotencyKey` 的入参摘要——同键换一套验收策略会被 fail-closed 拒绝,而不是 返回一个策略不同的旧任务。 - **报告轮次 0-based**:`report-N.*` 从 0 起;`get_task_report(round=0)` 合法,缺省返回最新。 - **手动 `verify_task(taskId)`**:自动分配下一可用轮次写入任务目录,不覆盖已有 `report-0.*`。 - **baselineRef**:`verify_task` 可传 Git ref 或任务 ID(taskId 场景默认读取该任务动工前基线);无效 ref 返回结构化错误,不静默退回当前 HEAD。 ## 与 tasks 结合 - 每轮验收产出 `report-N.md` + `report-N.json`(N 为轮次),存于该任务数据目录 `tasks//`。 - `report.json.checks[]` 每条含 `{name, cmd, passed, durationMs, exitCode, outputTail, timeout, skipped, reason}`。 - 变更/diffstat/可疑标记在 `report.json.analysis` 段,全部相对 **run_task/rework_task 动工前 git 基线**。 - `report.json.repairDirectives`(**仅失败轮次**,issue #19):把失败原因解析为可直接执行的动作, 形如 `{ items: [{file?, line?, issue, action, source}], sources, fallbackReason? }`。 `fallbackReason` 非空 ⇒ 结构化提取不可用,返修计划会显式回退到完整报告。 详见 [结构化修复指令](repair-directives.md)。 ## visual 段:AI 内容校验字段 `visual.contents[]`(图片内容规则)、`visual.content`(全局命令与预算)、`pages[].content` 与 `pages[].pixel` 为可选字段,**默认关闭**,需显式启用。判定委托用户自备命令,MCP 不读取任何密钥。 | 字段 | 默认 | 说明 | |---|---|---| | `visual.content.enabled` | `false` | 总开关;声明了任何内容规则却不启用会被 schema 拒绝(禁止「声明了却静默不跑」) | | `visual.content.command` | 无 | 自备判定命令;逐规则可用 `command` 覆盖,两者都缺则拒绝 | | `visual.content.argsTemplate` | 无 | 参数模板;占位符仅允许 ``、``、`` | | `visual.content.cwd` | 项目根 | 项目相对路径 | | `visual.content.env` | `{}` | `{ 子进程变量名: 宿主环境变量名 }`,缺失宿主变量 → 整轮阻塞 | | `visual.content.allowRemote` | `false` | 默认禁止外发;未放行的规则使用 `` 会被 schema 拒绝 | | `visual.content.samples` | `3` | 采样次数(1~9),多数票 | | `visual.content.timeoutMs` | `90000` | 单项超时;硬约束 `samples × timeoutMs ≤ limits.roundTimeoutMs` | | `visual.content.minConfidence` | 省略 | 省略即关闭置信度闸门;命令不报 confidence 时闸门不生效(报告会标注) | | `visual.content.cache` | `true` | 任务目录级判定缓存 | | `visual.contents[].id` | 必填 | 规则 ID,与既有 pages/images/viewports 的大小写不敏感去重 | | `visual.contents[].files` | 必填 | 图片项目相对路径列表(大小写不敏感去重) | | `visual.contents[].expect` | 必填 | 期望描述(1~4000 字符) | | `visual.contents[].blocking` | `false` | `false` 映射为 `optional:true`(仅告警);`true` 参与致败与返修 | | `visual.contents[].samples` / `allowRemote` / `command` / `argsTemplate` / `cwd` / `env` | 继承全局 | 逐规则覆盖 | | `visual.pages[].pixel` | `true` | `false` 表示语义-only:跳过像素对比与基准要求(必须声明 `content`,且不得同时声明 `baseline`/`pixelThreshold`/`maxDiffRatio`) | | `visual.pages[].content` | 无 | 页面级内容校验,复用同一次截图;派生 id `-content` 不得与已声明 id 冲突 | 启用闸门放宽为「至少 `pages` / `images` / `contents` 之一」。原因码、判定防抖、成本边界与外发声明见 [视觉验收](visual-acceptance.md) 的「AI 内容校验」章节。 ## 常见问题 - **验收命令找不到 node/npx**:tianshu-mcp 子进程会显式继承并前置 PATH(含系统 node 目录与天枢自带 node 目录)。若仍异常,检查你的 PATH。 - **想临时加验**:`verify_task(taskId, extraChecks=[{name:"x", cmd:["…"]}])`,不改文件。 - **不想让某脚本缺席导致一堆 skip**:接受即可,skip 不算失败,报告会标注缺失原因。