tianshu-mcp 0.6.4 → 0.6.5
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.en.md +30 -0
- package/CHANGELOG.md +30 -0
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/dist/config/acceptance-merge.js +36 -0
- package/dist/config/cli.js +160 -0
- package/dist/config/schema.js +44 -23
- package/dist/index.js +6 -0
- package/dist/loop/fix-loop.js +2 -0
- package/dist/mcp/handlers.js +13 -0
- package/dist/tasks/task-manager.js +1 -0
- package/dist/verify/acceptance.js +35 -18
- package/dist/version.generated.js +1 -1
- package/dist/visual/config.js +27 -1
- package/package.json +1 -1
package/CHANGELOG.en.md
CHANGED
|
@@ -8,6 +8,36 @@ Chinese version: [CHANGELOG.md](CHANGELOG.md)
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## [0.6.5] - 2026-09-24
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Three-level acceptance config inheritance** ([issue #20](https://github.com/lanlan0811/tianshu-mcp/issues/20)): `<data home>/acceptance.default.json` (global fallback) → `<project>/.tianshu-mcp/acceptance.json` (project override) → transient task-level override. With several similar projects under one host, the shared policy goes in the global layer instead of a file per project. See the [acceptance config spec](docs/acceptance-config.en.md).
|
|
16
|
+
- **New optional `acceptanceOverride` on `run_task` / `verify_task`**: a transient task-level acceptance-config override (same format as `acceptance.json`) that applies **only to that task**, is saved with the task snapshot, writes no `acceptance*.json` and affects no other task on the same project. Project-less mode explicitly rejects the argument.
|
|
17
|
+
- **`tianshu-mcp config acceptance [projectPath] [--task <taskId>]` debug command**: prints which layers exist, the effective order and the final values, so troubleshooting needs no guesswork. Each acceptance round also writes a same-source summary line to `server.log`.
|
|
18
|
+
- New `src/config/acceptance-merge.ts` (a purpose-scoped merge helper, **deliberately not a general-purpose deep merge**).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **The `.default()` pollution hazard in layered parsing**: `AcceptanceConfigSchema` puts `.default(true)` on `requireChanges`, so parsing a project file that only sets `verifyConcurrency` materializes `requireChanges: true`, which in the three-level chain would **in turn override the global layer's `false`**. A default-free `PartialAcceptanceConfigSchema` is now used for layered parsing, with defaults applied only when the final value is absent.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `resolveChecks()` now performs the three-level merge and returns the merged `visual` too; `executeVerify` no longer re-reads the project file (otherwise the `visual` from override/global layers would change meaning based on whether a project file happens to exist).
|
|
27
|
+
- After the unified `LOCKFILE_PATTERN` export (v0.6.4), the bilingual `acceptance-config` precedence tables were rewritten around the three-level chain.
|
|
28
|
+
|
|
29
|
+
### Compatibility
|
|
30
|
+
|
|
31
|
+
- **No tool contract, data model or MCP annotation changes** (`acceptanceOverride` is a new optional argument; `extraChecks` semantics and precedence are unchanged and still rank above the base set).
|
|
32
|
+
- Without creating a global `acceptance.default.json`, single-project behaviour is exactly as in v0.6.4 (an absent global layer = empty config, no error).
|
|
33
|
+
- The existing semantics of the project-level `.tianshu-mcp/acceptance.json` are unchanged; error semantics remain fail-closed (only `ENOENT` counts as "layer absent").
|
|
34
|
+
|
|
35
|
+
### Notes (disclosed honestly)
|
|
36
|
+
|
|
37
|
+
- **Merge granularity is per field**: a field a higher layer explicitly writes wins outright; **arrays (`checks`) replace wholesale rather than concatenating** — concatenating would turn "the project adds one check" into "the project can never remove a global check".
|
|
38
|
+
- **`visual` replaces wholesale and is not deep-merged across layers**: nearly every field of the `visual` schema carries a default, so deep merging would let a higher layer's "not written, present only as a default" fields silently clobber a lower layer's **explicit** values (the same pollution class as `requireChanges`). To reuse visual config across layers, write the full `visual` block in the project layer. **This is a deliberate deviation from the issue's suggested "deep merge"**, with the reasoning recorded in `docs/acceptance-config.en.md` and ARCHITECTURE §7.4.
|
|
39
|
+
- **The task override is included in the idempotency argument digest**: replaying the same key with a different acceptance policy is rejected fail-closed rather than returning an old task built on a different policy.
|
|
40
|
+
|
|
11
41
|
## [0.6.4] - 2026-09-24
|
|
12
42
|
|
|
13
43
|
### Added
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.6.5] - 2026-09-24
|
|
11
|
+
|
|
12
|
+
### 新增
|
|
13
|
+
|
|
14
|
+
- **验收配置三级继承**([issue #20](https://github.com/lanlan0811/tianshu-mcp/issues/20)):`<数据目录>/acceptance.default.json`(全局兜底)→ `<project>/.tianshu-mcp/acceptance.json`(项目覆盖)→ 任务级临时覆盖。一个宿主下挂多个同类项目时,公共策略写全局层即可,不必逐项目建文件。详见 [验收配置规范](docs/acceptance-config.md)。
|
|
15
|
+
- **`run_task` / `verify_task` 新增可选 `acceptanceOverride`**:任务级临时验收配置覆盖(格式同 `acceptance.json`),**仅当次生效**、随任务快照保存、不写入任何 `acceptance*.json`、不影响同项目其他任务。无项目模式显式拒绝该参数。
|
|
16
|
+
- **`tianshu-mcp config acceptance [projectPath] [--task <taskId>]` 调试命令**:打印各层是否存在、实际生效顺序与最终取值,排障无需靠猜。每轮验收另往 `server.log` 写一行同源摘要。
|
|
17
|
+
- 新增 `src/config/acceptance-merge.ts`(按用途收敛的合并工具,**刻意不做通用深合并**)。
|
|
18
|
+
|
|
19
|
+
### 修复
|
|
20
|
+
|
|
21
|
+
- **分层解析的 `.default()` 污染隐患**:`AcceptanceConfigSchema` 的 `requireChanges` 带 `.default(true)`,若用它解析「只写了 `verifyConcurrency`」的项目文件会 materialize 出 `requireChanges: true`,在三级继承里**反过来覆盖全局层的 `false`**。新增无默认值的 `PartialAcceptanceConfigSchema` 供分层解析,默认值只在最终取值缺省时兜底。
|
|
22
|
+
|
|
23
|
+
### 变更
|
|
24
|
+
|
|
25
|
+
- `resolveChecks()` 改为三级合并,并把合并后的 `visual` 一并返回;`executeVerify` 不再二次读取项目文件(否则 override/全局层的 `visual` 会随项目文件是否存在而改变语义)。
|
|
26
|
+
- `LOCKFILE_PATTERN` 的统一导出(v0.6.4)之后,`acceptance-config` 双语优先级表按三级继承重写。
|
|
27
|
+
|
|
28
|
+
### 兼容性
|
|
29
|
+
|
|
30
|
+
- **无工具契约、数据模型或 MCP 注解变更**(`acceptanceOverride` 是新增可选参数;`extraChecks` 语义与优先级不变,仍高于基础集)。
|
|
31
|
+
- 不创建全局 `acceptance.default.json` 时,单项目行为与 v0.6.4 完全一致(全局层缺失 = 空配置、不报错)。
|
|
32
|
+
- 项目级 `.tianshu-mcp/acceptance.json` 的既有语义不变;错误语义仍是 fail-closed(仅 `ENOENT` 视为「该层不存在」)。
|
|
33
|
+
|
|
34
|
+
### 说明(如实披露)
|
|
35
|
+
|
|
36
|
+
- **合并粒度是「字段」**:高优先级层显式书写的字段整体取胜;**数组(`checks`)整体覆盖而非拼接** —— 拼接会让「项目追加一项检查」变成「项目无法移除全局检查」。
|
|
37
|
+
- **`visual` 整体覆盖、不做跨层深合并**:`visual` 的 schema 几乎每个字段都带默认值,深合并会让低优先级层的**显式**取值被高优先级层「未书写、仅因默认值而出现」的字段静默覆盖(与 `requireChanges` 同类的污染)。需要跨层复用视觉配置时请在项目层写完整 `visual` 段。**这是与 issue 建议的「深合并」的一处有意偏离**,理由记录在 `docs/acceptance-config.md` 与 ARCHITECTURE §7.4。
|
|
38
|
+
- **任务级覆盖计入幂等入参摘要**:同键换一套验收策略会被 fail-closed 拒绝,而不是返回策略不同的旧任务。
|
|
39
|
+
|
|
10
40
|
## [0.6.4] - 2026-09-24
|
|
11
41
|
|
|
12
42
|
### 新增
|
package/README.en.md
CHANGED
|
@@ -43,6 +43,7 @@ Tianshu plays the role of the overall commander; this MCP server is the **schedu
|
|
|
43
43
|
- **Structured repair directives (issue #19)**: failed rounds parse the reasons into **directly executable actions** (`file:line / issue / action`) carried in both the repair plan and the rework message, sparing the agent the cost of locating problems in a full narrative report; when extraction is unavailable it **explicitly falls back** to the full report (never a silent gap). `rework_task` also accepts an optional `repairHint`. See [structured repair directives](docs/repair-directives.en.md).
|
|
44
44
|
- **Objective acceptance**: automated command checks (typecheck/lint/test/build — skipped when absent, plus tech-stack derivation) + programmatic code analysis (changed-file list / diffstat / suspicious signals such as TODO, debugger, secret-like patterns), all relative to a **git baseline**; never auto-commits or stashes. The acceptance engine is **fail-closed**: a test check fails when its output reports zero executed tests even if the exit code is 0; git projects must produce changes relative to the pre-work baseline by default (pure analysis tasks can opt out with `"requireChanges": false` in `.tianshu-mcp/acceptance.json`).
|
|
45
45
|
- **Acceptance parallelism**: command checks run **bounded-parallel** by default (`verifyConcurrency`, default 2, range 1–4). When checks depend on an order (a later check reading build output, `--fix`, shared cache dirs), set it to `1` for fully serial behaviour; a project can override it in `.tianshu-mcp/acceptance.json`, and the server level lives in `config.json`. Report and log formats are unchanged (results are returned in declaration order).
|
|
46
|
+
- **Three-level acceptance config inheritance (issue #20)**: `<data home>/acceptance.default.json` (global fallback) → `<project>/.tianshu-mcp/acceptance.json` (project override) → the `acceptanceOverride` argument of `run_task`/`verify_task` (transient task override, applying only to that task and never written to disk as config). For the same field a higher layer wins; arrays replace wholesale rather than concatenating. Inspect the final effective configuration with `tianshu-mcp config acceptance <projectPath> [--task <id>]`. See the [acceptance config spec](docs/acceptance-config.en.md).
|
|
46
47
|
- **Rework loop**: automatic rework (`autoFixRounds`) + manual `rework_task`; on verification failure a repair-plan file is generated and fed back to the agent; when rounds run out → `needs_attention` awaiting Tianshu's verdict.
|
|
47
48
|
- **Execution surfaces**: `driver: "gui"` selects an explicit, isolated Codex/TraeWork/ZCode/Kimi Code/Qoder CN CDP adapter; `driver: "spawn"` runs an external CLI child process.
|
|
48
49
|
- **Project-less dispatch (ZCode, issue #12)**: `run_task`'s `projectPath` may be omitted — ZCode runs the task in its `default` workspace without registering/importing a project, collecting a Git baseline, or running project acceptance (the result is marked structurally as `verificationNotApplicable: "no_project"` and `verify_task`/`get_task_report` return a not-applicable explanation). The companion `allowCreateProject: false` stops dispatch before any import side effect when the target directory is unregistered. See the [ZCode CDP adapter](docs/zcode-cdp.en.md).
|
|
@@ -188,7 +189,7 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
188
189
|
|
|
189
190
|
| Tool | Capability / approval | Purpose |
|
|
190
191
|
|---|---|---|
|
|
191
|
-
| `run_task` | write + approval | Dispatch work (optional auto-verify / auto-rework); returns `taskId` asynchronously. Optional `idempotencyKey
|
|
192
|
+
| `run_task` | write + approval | Dispatch work (optional auto-verify / auto-rework); returns `taskId` asynchronously. Optional `idempotencyKey` (a retry with the same key returns the original `taskId` instead of creating a task) and `acceptanceOverride` (transient acceptance-config override applying only to this task) |
|
|
192
193
|
| `continue_task` | write + approval | Resume the session behind `needs_user` (ZCode resumes the recorded session; Codex re-observes for `user_confirmation` / re-dispatches for `login_required`; Kimi Code resumes the recorded session and distinguishes question answering / re-observation / full re-dispatch) |
|
|
193
194
|
| `query_task` | read | Poll status / progress / log tail / recent fine-grained events. The optional `eventLimit` (1..50, default 10) controls how many `recentEvents` the meta block carries, so long tasks can be told apart as "working normally" versus "stuck on a dialog waiting for a human" |
|
|
194
195
|
| `list_tasks` | read | Filtered history of tasks |
|
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
- **长任务可观测(issue #18)**:适配器在关键节点上报细粒度事件(`task_dispatched` / `confirmation_dialog_detected` / `awaiting_user_authorization` / `file_modification_started` / `rework_triggered`),`query_task` 经 `eventLimit`(默认 10)回传最近 N 条——**能区分「agent 正在干活」与「卡在弹窗等人工介入」**。事件上报是可选能力:未实现的适配器行为不变。本版 codex 与 traework 已落地上报。详见 [事件流](docs/event-stream.md)。
|
|
43
43
|
- **客观验收**:自动命令检查(typecheck/lint/test/build,缺则跳过 + 技术栈推导)+ 程序化代码分析(变更清单/diffstat/TODO·debugger·密钥形态等可疑标记),全部相对 **git 基线**,不自动 commit/stash。验收引擎 **fail-closed**:测试命令退出码为 0 但输出显示零用例时判失败;git 项目默认要求相对动工前基线产生变更(纯分析任务可在 `.tianshu-mcp/acceptance.json` 设 `"requireChanges": false` 显式关闭)。
|
|
44
44
|
- **验收并行度**:命令检查默认**有界并行**(`verifyConcurrency`,默认 2、范围 1–4)。检查项之间有顺序依赖时(后续检查读取 build 产物、带 `--fix`、共享缓存目录)请设 `1` 完全退化为串行;项目级 `.tianshu-mcp/acceptance.json` 可覆盖,server 级在 `config.json`。报告与日志格式不变(结果按声明顺序返回)。
|
|
45
|
+
- **验收配置三级继承(issue #20)**:`<数据目录>/acceptance.default.json`(全局兜底)→ `<project>/.tianshu-mcp/acceptance.json`(项目覆盖)→ `run_task`/`verify_task` 的 `acceptanceOverride` 参数(任务级临时覆盖,仅当次生效、不落盘为配置)。同名字段高优先级取胜、数组整体覆盖不拼接。用 `tianshu-mcp config acceptance <projectPath> [--task <id>]` 查看最终生效配置。详见 [验收配置规范](docs/acceptance-config.md)。
|
|
45
46
|
- **失败返修闭环**:自动返修(`autoFixRounds`)+ 手动 `rework_task`;验收失败时自动生成修复计划文件并回填给 agent;轮次用尽 → `needs_attention` 等天枢裁决。
|
|
46
47
|
- **结构化修复指令(issue #19)**:失败轮次会把原因解析为**可直接执行的动作**(`文件:行 / 问题 / 做什么`),随返修计划与返修消息一起喂给 agent,省去它从整篇报告里定位的开销;提取不到时**显式回退**到完整报告(不静默留空)。`rework_task` 另可选 `repairHint` 自带提示。详见 [结构化修复指令](docs/repair-directives.md)。
|
|
47
48
|
- **执行面**:`driver: "gui"` 由显式 adapter 驱动桌面 UI(Codex / TraeWork / ZCode / Kimi Code 各自使用隔离的 CDP 流程);`driver: "spawn"` 走外部 CLI 子进程。
|
|
@@ -184,7 +185,7 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
184
185
|
|
|
185
186
|
| 工具 | 能力 / 审批 | 作用 |
|
|
186
187
|
|---|---|---|
|
|
187
|
-
| `run_task` | write + 审批 | 派活(可带自动验收/自动返修),异步返回 `taskId`;可选 `idempotencyKey
|
|
188
|
+
| `run_task` | write + 审批 | 派活(可带自动验收/自动返修),异步返回 `taskId`;可选 `idempotencyKey`(同键重试恒返回原 `taskId`,不新建任务)与 `acceptanceOverride`(任务级临时验收配置覆盖,仅本任务生效) |
|
|
188
189
|
| `continue_task` | write + 审批 | 恢复 `needs_user` 的原会话(ZCode 恢复原会话;Codex 按 `user_confirmation` 重新观察 / `login_required` 重派;Kimi Code 恢复原会话并区分提问续答 / 重新观察 / 补发任务书) |
|
|
189
190
|
| `query_task` | read | 轮询状态 / 进度 / 日志尾 / 最近细粒度事件。可选 `eventLimit`(1..50,默认 10)控制 meta 的 `recentEvents` 条数,长任务下可区分「正常执行」与「卡在弹窗等人」 |
|
|
190
191
|
| `list_tasks` | read | 历史任务过滤列表 |
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 合并两层配置:override 中**显式书写**的字段整体取代 base 的同名字段。
|
|
3
|
+
* `undefined` 视为「本层未书写」,不参与覆盖(否则 `{requireChanges: undefined}` 会误清空下层取值)。
|
|
4
|
+
*/
|
|
5
|
+
export function mergeAcceptanceConfig(base, override) {
|
|
6
|
+
const out = { ...(base ?? {}) };
|
|
7
|
+
const target = out;
|
|
8
|
+
for (const [key, value] of Object.entries(override ?? {})) {
|
|
9
|
+
if (value !== undefined)
|
|
10
|
+
target[key] = value;
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
/** 按数组顺序(低 → 高优先级)依次合并;缺失层直接跳过 */
|
|
15
|
+
export function resolveAcceptanceLayers(layers) {
|
|
16
|
+
let config = {};
|
|
17
|
+
const applied = [];
|
|
18
|
+
for (const layer of layers) {
|
|
19
|
+
if (!layer.config)
|
|
20
|
+
continue;
|
|
21
|
+
config = mergeAcceptanceConfig(config, layer.config);
|
|
22
|
+
applied.push({ source: layer.source, path: layer.path });
|
|
23
|
+
}
|
|
24
|
+
return { config, applied };
|
|
25
|
+
}
|
|
26
|
+
/** 供调试命令/日志使用的一行摘要(不打印任何凭证,只打层与最终取值) */
|
|
27
|
+
export function summarizeResolved(resolved) {
|
|
28
|
+
const c = resolved.config;
|
|
29
|
+
return [
|
|
30
|
+
`生效层=${resolved.applied.map((a) => a.source).join(">") || "(无)"}`,
|
|
31
|
+
`checks=${c.checks ? c.checks.length : "(默认集)"}`,
|
|
32
|
+
`requireChanges=${c.requireChanges ?? "(默认 true)"}`,
|
|
33
|
+
`verifyConcurrency=${c.verifyConcurrency ?? "(继承 server config)"}`,
|
|
34
|
+
`visual=${c.visual ? "已覆盖" : "(未配置)"}`,
|
|
35
|
+
].join(" ");
|
|
36
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tianshu-mcp config …` CLI 子命令(issue #20)。
|
|
3
|
+
*
|
|
4
|
+
* 目的:三级继承(全局 / 项目 / 任务级覆盖)生效后,「最终到底用了哪几层、每层贡献了什么」
|
|
5
|
+
* 必须可查——否则排障只能靠猜。输出走 `console.log`,**绝不触碰协议 stdout**
|
|
6
|
+
* (与 `visual/cli.ts` 同一约定;本命令在创建 MCP server 之前返回)。
|
|
7
|
+
*
|
|
8
|
+
* 不挂在 `visual` 命名空间下:`acceptance.json` 是验收引擎的配置,视觉验收只是共用一个文件,
|
|
9
|
+
* 混进 visual 会误导使用者。
|
|
10
|
+
*
|
|
11
|
+
* 用法:
|
|
12
|
+
* tianshu-mcp config acceptance [projectPath] [--task <taskId>]
|
|
13
|
+
*/
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { readAcceptanceLayer } from "../visual/config.js";
|
|
16
|
+
import { resolveAcceptanceLayers, summarizeResolved } from "./acceptance-merge.js";
|
|
17
|
+
import { resolveDataHome } from "./store.js";
|
|
18
|
+
import { readJsonSafe } from "../util/fs.js";
|
|
19
|
+
export async function runConfigCli(args) {
|
|
20
|
+
const [scope, ...rest] = args;
|
|
21
|
+
if (scope !== "acceptance") {
|
|
22
|
+
console.log(JSON.stringify({
|
|
23
|
+
ok: false,
|
|
24
|
+
error: `未知的 config 子命令: ${scope ?? "(空)"}`,
|
|
25
|
+
usage: [
|
|
26
|
+
"tianshu-mcp config acceptance [projectPath] [--task <taskId>]",
|
|
27
|
+
"",
|
|
28
|
+
"打印验收配置三级继承(全局 / 项目 / 任务级覆盖)的生效情况与最终取值。",
|
|
29
|
+
],
|
|
30
|
+
}, null, 2));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const result = await inspectAcceptance(rest);
|
|
34
|
+
console.log(JSON.stringify(result, null, 2));
|
|
35
|
+
}
|
|
36
|
+
/** 解析 `[projectPath] [--task <taskId>]`(顺序无关,两者都可省) */
|
|
37
|
+
function parseArgs(rest) {
|
|
38
|
+
let projectPath;
|
|
39
|
+
let taskId;
|
|
40
|
+
for (let i = 0; i < rest.length; i++) {
|
|
41
|
+
const a = rest[i];
|
|
42
|
+
if (a === "--task") {
|
|
43
|
+
taskId = rest[i + 1];
|
|
44
|
+
if (!taskId)
|
|
45
|
+
return { error: "--task 需要一个 taskId 参数" };
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
else if (a.startsWith("--")) {
|
|
49
|
+
return { error: `未知选项: ${a}` };
|
|
50
|
+
}
|
|
51
|
+
else if (projectPath === undefined) {
|
|
52
|
+
projectPath = a;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
return { error: `多余的参数: ${a}` };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { projectPath, taskId };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 收集并解析三层配置。**不抛错**:某层写坏时把错误放进该层的 `error` 字段并让整体
|
|
62
|
+
* `ok: false`,这样调试命令仍然能把「是哪一层坏了」显示出来(而不是只吐一句异常)。
|
|
63
|
+
*/
|
|
64
|
+
export async function inspectAcceptance(rest) {
|
|
65
|
+
const parsed = parseArgs(rest);
|
|
66
|
+
if (parsed.error) {
|
|
67
|
+
return { ok: false, error: parsed.error, layers: [], appliedOrder: [], effective: emptyEffective(), summary: "" };
|
|
68
|
+
}
|
|
69
|
+
const home = resolveDataHome();
|
|
70
|
+
const globalPath = path.join(home, "acceptance.default.json");
|
|
71
|
+
const projectPath = parsed.projectPath ? path.resolve(parsed.projectPath) : undefined;
|
|
72
|
+
const projectFile = projectPath ? path.join(projectPath, ".tianshu-mcp", "acceptance.json") : undefined;
|
|
73
|
+
const layers = [];
|
|
74
|
+
let taskError;
|
|
75
|
+
// 层的 label 统一用 source 名(global / project):它与 CLI 输出的 source 字段、
|
|
76
|
+
// 以及验收引擎报错文案里的层名同源,便于按字符串定位是哪一层出了问题。
|
|
77
|
+
const global = await readLayerInto(layers, "global", globalPath);
|
|
78
|
+
const project = projectFile
|
|
79
|
+
? await readLayerInto(layers, "project", projectFile)
|
|
80
|
+
: (layers.push({ source: "project", path: "(未提供 projectPath)", present: false }),
|
|
81
|
+
undefined);
|
|
82
|
+
// 任务级覆盖来自任务快照(issue #20 把 override 持久化在 TaskMeta 上)
|
|
83
|
+
let override;
|
|
84
|
+
if (parsed.taskId) {
|
|
85
|
+
const metaPath = path.join(home, "tasks", parsed.taskId, "task.json");
|
|
86
|
+
const meta = await readJsonSafe(metaPath);
|
|
87
|
+
if (!meta) {
|
|
88
|
+
layers.push({ source: "override", path: metaPath, present: false });
|
|
89
|
+
taskError = `任务快照不可读或不存在: ${parsed.taskId}`;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
override = meta.acceptanceOverride;
|
|
93
|
+
layers.push({
|
|
94
|
+
source: "override",
|
|
95
|
+
path: `${metaPath}(TaskMeta.acceptanceOverride)`,
|
|
96
|
+
present: override !== undefined,
|
|
97
|
+
...(override ? { config: override } : {}),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
layers.push({ source: "override", path: "(未提供 --task,无任务级覆盖)", present: false });
|
|
103
|
+
}
|
|
104
|
+
const mergedLayers = [
|
|
105
|
+
{ source: "global", path: globalPath, config: global },
|
|
106
|
+
{ source: "project", path: projectFile ?? "(未提供)", config: project },
|
|
107
|
+
{ source: "override", path: "TaskMeta.acceptanceOverride", config: override },
|
|
108
|
+
];
|
|
109
|
+
const resolved = resolveAcceptanceLayers(mergedLayers);
|
|
110
|
+
const anyLayerError = layers.some((l) => l.error);
|
|
111
|
+
const ok = !anyLayerError && !taskError;
|
|
112
|
+
return {
|
|
113
|
+
ok,
|
|
114
|
+
...(projectPath ? { project: projectPath } : {}),
|
|
115
|
+
...(parsed.taskId ? { taskId: parsed.taskId } : {}),
|
|
116
|
+
layers,
|
|
117
|
+
appliedOrder: resolved.applied.map((a) => a.source),
|
|
118
|
+
effective: {
|
|
119
|
+
checks: resolved.config.checks ? resolved.config.checks.length : "(默认集)",
|
|
120
|
+
requireChanges: resolved.config.requireChanges ?? "(默认 true)",
|
|
121
|
+
verifyConcurrency: resolved.config.verifyConcurrency ?? "(继承 server config)",
|
|
122
|
+
visual: resolved.config.visual ? "已覆盖" : "(未配置)",
|
|
123
|
+
},
|
|
124
|
+
summary: summarizeResolved(resolved),
|
|
125
|
+
...(taskError ? { error: taskError } : {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function readLayerInto(layers, source, absPath) {
|
|
129
|
+
try {
|
|
130
|
+
const config = await readAcceptanceLayer(absPath, source);
|
|
131
|
+
layers.push({
|
|
132
|
+
source,
|
|
133
|
+
path: absPath,
|
|
134
|
+
present: config !== null,
|
|
135
|
+
...(config ? { config } : {}),
|
|
136
|
+
});
|
|
137
|
+
return config ?? undefined;
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
// 该层写坏/读不了:如实登记为错误层,其余层继续解析(调试命令的价值正在于此)
|
|
141
|
+
layers.push({
|
|
142
|
+
source,
|
|
143
|
+
path: absPath,
|
|
144
|
+
present: true,
|
|
145
|
+
error: {
|
|
146
|
+
code: e.code ?? "CONFIG_ERROR",
|
|
147
|
+
message: e instanceof Error ? e.message : String(e),
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function emptyEffective() {
|
|
154
|
+
return {
|
|
155
|
+
checks: "(默认集)",
|
|
156
|
+
requireChanges: "(默认 true)",
|
|
157
|
+
verifyConcurrency: "(继承 server config)",
|
|
158
|
+
visual: "(未配置)",
|
|
159
|
+
};
|
|
160
|
+
}
|
package/dist/config/schema.js
CHANGED
|
@@ -47,6 +47,40 @@ export const IdempotencyScopeSchema = z.enum(["run_task", "verify_task"]);
|
|
|
47
47
|
/** 幂等映射默认 TTL(24h)与条目上限(超限逐出最旧)。 */
|
|
48
48
|
export const IDEMPOTENCY_TTL_DEFAULT_MS = 24 * 60 * 60_000;
|
|
49
49
|
export const IDEMPOTENCY_MAX_ENTRIES_DEFAULT = 2000;
|
|
50
|
+
/**
|
|
51
|
+
* 验收命令:**推荐数组形态**(结构化 argv,无歧义)。
|
|
52
|
+
* 字符串形态为兼容保留:经极简分词(不经过 shell),**不支持转义**,引号不闭合
|
|
53
|
+
* 不报错,含空格参数必须手写整段引号,写错会静默拆成多个 argv——详见 docs/acceptance-config.md。
|
|
54
|
+
*/
|
|
55
|
+
const CheckCmd = z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]);
|
|
56
|
+
export const AcceptanceCheckSchema = z.object({
|
|
57
|
+
name: z.string().min(1),
|
|
58
|
+
cmd: CheckCmd,
|
|
59
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
60
|
+
optional: z.boolean().optional(),
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* **分层**验收配置(issue #20):每个字段都可选、**任何字段都不带 `.default()`**。
|
|
64
|
+
*
|
|
65
|
+
* 为什么必须与 `AcceptanceConfigSchema` 分开:`requireChanges` 在后者上有 `.default(true)`。
|
|
66
|
+
* 若用带默认值的 schema 去解析「只写了 verifyConcurrency」的项目文件,会 materialize 出
|
|
67
|
+
* `requireChanges: true`,在三级继承里**反过来把全局层的 `false` 覆盖掉** —— 这正是本次要修的隐患。
|
|
68
|
+
* 分层解析一律用本 schema,默认值只在最终生效值缺省时由消费方兜底。
|
|
69
|
+
*/
|
|
70
|
+
export const PartialAcceptanceConfigSchema = z.object({
|
|
71
|
+
checks: z.array(AcceptanceCheckSchema).optional(),
|
|
72
|
+
/** 注意:`visual` **不做跨层深合并**,最高优先级的层整段生效(理由见 docs/acceptance-config.md) */
|
|
73
|
+
visual: VisualConfigSchema.optional(),
|
|
74
|
+
requireChanges: z.boolean().optional(),
|
|
75
|
+
verifyConcurrency: z
|
|
76
|
+
.number()
|
|
77
|
+
.transform((n) => (Number.isFinite(n) ? Math.min(4, Math.max(1, Math.round(n))) : 1))
|
|
78
|
+
.optional(),
|
|
79
|
+
});
|
|
80
|
+
/** 兼容形态:在分层 schema 之上补回 `requireChanges` 的历史默认值(供既有 visual/legacy 调用方) */
|
|
81
|
+
export const AcceptanceConfigSchema = PartialAcceptanceConfigSchema.extend({
|
|
82
|
+
requireChanges: z.boolean().default(true),
|
|
83
|
+
});
|
|
50
84
|
export const RunTaskParamsSchema = z.object({
|
|
51
85
|
/**
|
|
52
86
|
* 项目绝对路径。**省略** = 无项目模式(issue #12):目前仅 ZCode 支持——任务在其 `default`
|
|
@@ -99,6 +133,12 @@ export const RunTaskParamsSchema = z.object({
|
|
|
99
133
|
* 不新建任务;同一键携带不同参数会被 fail-closed 拒绝。不传 = 保持既有行为。
|
|
100
134
|
*/
|
|
101
135
|
idempotencyKey: IdempotencyKeySchema.optional(),
|
|
136
|
+
/**
|
|
137
|
+
* 任务级临时验收配置覆盖(issue #20):三级继承的最高优先级,**仅当次任务生效**。
|
|
138
|
+
* 会随任务快照保存(属任务数据,不是配置文件),rework/continue 沿用同一任务时继续生效,
|
|
139
|
+
* 不影响其他任务或项目。不传则完全沿用全局/项目层配置。
|
|
140
|
+
*/
|
|
141
|
+
acceptanceOverride: PartialAcceptanceConfigSchema.optional(),
|
|
102
142
|
});
|
|
103
143
|
/** query_task 返回的细粒度事件条数默认值(issue #18) */
|
|
104
144
|
export const QUERY_TASK_EVENT_LIMIT_DEFAULT = 10;
|
|
@@ -126,18 +166,6 @@ export const CancelTaskParamsSchema = z.object({
|
|
|
126
166
|
taskId: z.string().min(1),
|
|
127
167
|
reason: z.string().optional(),
|
|
128
168
|
});
|
|
129
|
-
/**
|
|
130
|
-
* 验收命令:**推荐数组形态**(结构化 argv,无歧义)。
|
|
131
|
-
* 字符串形态为兼容保留:经极简分词(不经过 shell),**不支持转义**,引号不闭合
|
|
132
|
-
* 不报错,含空格参数必须手写整段引号,写错会静默拆成多个 argv——详见 docs/acceptance-config.md。
|
|
133
|
-
*/
|
|
134
|
-
const CheckCmd = z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]);
|
|
135
|
-
export const AcceptanceCheckSchema = z.object({
|
|
136
|
-
name: z.string().min(1),
|
|
137
|
-
cmd: CheckCmd,
|
|
138
|
-
timeoutMs: z.number().int().positive().optional(),
|
|
139
|
-
optional: z.boolean().optional(),
|
|
140
|
-
});
|
|
141
169
|
export const VerifyTaskParamsSchema = z.object({
|
|
142
170
|
taskId: z.string().optional(),
|
|
143
171
|
projectPath: AbsPath.optional(),
|
|
@@ -155,6 +183,8 @@ export const VerifyTaskParamsSchema = z.object({
|
|
|
155
183
|
* 已完成直接返回已有报告与轮次;同一键携带不同参数会被 fail-closed 拒绝。
|
|
156
184
|
*/
|
|
157
185
|
idempotencyKey: IdempotencyKeySchema.optional(),
|
|
186
|
+
/** 任务级临时验收配置覆盖(issue #20):三级继承的最高优先级,仅本次验收生效。 */
|
|
187
|
+
acceptanceOverride: PartialAcceptanceConfigSchema.optional(),
|
|
158
188
|
});
|
|
159
189
|
export const ReworkTaskParamsSchema = z.object({
|
|
160
190
|
taskId: z.string().min(1),
|
|
@@ -427,14 +457,5 @@ export const ProjectRecordSchema = z.object({
|
|
|
427
457
|
});
|
|
428
458
|
/** projects.json 整体文件 schema(S5:不再用对象强制类型转换) */
|
|
429
459
|
export const ProjectsFileSchema = z.record(z.string().min(1), ProjectRecordSchema);
|
|
430
|
-
/*
|
|
431
|
-
|
|
432
|
-
checks: z.array(AcceptanceCheckSchema).optional(),
|
|
433
|
-
visual: VisualConfigSchema.optional(),
|
|
434
|
-
requireChanges: z.boolean().default(true),
|
|
435
|
-
/** 命令检查并行度:1=串行(与历史行为一致);缺省继承 server config.json 的 verifyConcurrency(默认 2)。越界值 clamp 到 1..4(不再株连整份 acceptance.json 失效) */
|
|
436
|
-
verifyConcurrency: z
|
|
437
|
-
.number()
|
|
438
|
-
.transform((n) => (Number.isFinite(n) ? Math.min(4, Math.max(1, Math.round(n))) : 1))
|
|
439
|
-
.optional(),
|
|
440
|
-
});
|
|
460
|
+
/* 项目内 .tianshu-mcp/acceptance.json 的 schema 定义见文件上方(须早于 RunTaskParamsSchema,
|
|
461
|
+
因为 run_task/verify_task 的 acceptanceOverride 参数引用它)。 */
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,12 @@ async function main() {
|
|
|
17
17
|
await runVisualCli(process.argv.slice(3));
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
+
// issue #20:验收配置三级继承的调试命令(在创建 server 之前返回,不触碰协议 stdout)
|
|
21
|
+
if (process.argv[2] === "config") {
|
|
22
|
+
const { runConfigCli } = await import("./config/cli.js");
|
|
23
|
+
await runConfigCli(process.argv.slice(3));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
20
26
|
const home = resolveDataHome();
|
|
21
27
|
const logger = await Logger.create(path.join(home, "logs"));
|
|
22
28
|
const skipSkillInstall = process.argv.includes("--no-skill-install") || process.env.TIANSHU_MCP_NO_SKILL_INSTALL === "1";
|
package/dist/loop/fix-loop.js
CHANGED
|
@@ -457,6 +457,8 @@ export class TaskOrchestrator {
|
|
|
457
457
|
round,
|
|
458
458
|
config: await this.deps.dataHome.loadConfig(),
|
|
459
459
|
projectVerify,
|
|
460
|
+
// issue #20:任务级临时覆盖(三级继承最高优先级),随任务快照持久化
|
|
461
|
+
acceptanceOverride: meta.acceptanceOverride,
|
|
460
462
|
baseline,
|
|
461
463
|
signal: this.signal,
|
|
462
464
|
store,
|
package/dist/mcp/handlers.js
CHANGED
|
@@ -63,6 +63,9 @@ function runTaskKeyedFields(args) {
|
|
|
63
63
|
autoVerify: args.autoVerify,
|
|
64
64
|
autoFixRounds: args.autoFixRounds,
|
|
65
65
|
taskTimeoutMs: args.taskTimeoutMs,
|
|
66
|
+
// issue #20:验收策略覆盖会改变这一轮的门禁,必须计入摘要 —— 否则同键重放
|
|
67
|
+
// 会返回一个「策略不同」的旧任务,调用方以为重试成功、实际拿到的是另一套验收口径。
|
|
68
|
+
acceptanceOverride: args.acceptanceOverride,
|
|
66
69
|
};
|
|
67
70
|
}
|
|
68
71
|
/** 幂等重放的响应行:如实回报既有任务的当前状态(含终态),不承诺会重新派发。 */
|
|
@@ -329,6 +332,8 @@ function runTaskHandler(ctx, defaults, idempotency) {
|
|
|
329
332
|
resolved.profile.gui?.defaultAutoFixRounds ??
|
|
330
333
|
defaults.defaultAutoFixRounds,
|
|
331
334
|
taskTimeoutMs,
|
|
335
|
+
// issue #20:任务级临时验收配置覆盖,随快照保存、仅本任务生效
|
|
336
|
+
acceptanceOverride: args.acceptanceOverride,
|
|
332
337
|
idempotencyKey: args.idempotencyKey,
|
|
333
338
|
idempotencyScope: args.idempotencyKey === undefined ? undefined : "run_task",
|
|
334
339
|
idempotencyDigest: args.idempotencyKey === undefined ? undefined : digest,
|
|
@@ -416,6 +421,10 @@ async function runTaskWithoutProject(ctx, defaults, args, idempotency) {
|
|
|
416
421
|
}
|
|
417
422
|
if (args.mode !== undefined)
|
|
418
423
|
return errorResult("ZCode 不支持 mode 参数;请移除 mode 后重试");
|
|
424
|
+
if (args.acceptanceOverride !== undefined) {
|
|
425
|
+
// 无项目模式没有项目验收,覆盖无的放矢;显式拒绝而非静默忽略,避免调用方误以为已生效
|
|
426
|
+
return errorResult("无项目模式不支持 acceptanceOverride:没有项目验收可覆盖。请提供 projectPath,或移除该参数。");
|
|
427
|
+
}
|
|
419
428
|
try {
|
|
420
429
|
parseZcodeModel(args.model);
|
|
421
430
|
// 无项目模式不做项目引用解析:识别到本地引用就在发送前说明需要 projectPath。
|
|
@@ -634,6 +643,8 @@ async function verifyIdempotencyDigest(args) {
|
|
|
634
643
|
checksMode: args.checksMode,
|
|
635
644
|
extraChecks: args.extraChecks,
|
|
636
645
|
baselineRef: args.baselineRef,
|
|
646
|
+
// issue #20:同键重放不得换一套验收策略,故计入摘要
|
|
647
|
+
acceptanceOverride: args.acceptanceOverride,
|
|
637
648
|
});
|
|
638
649
|
}
|
|
639
650
|
/** 同键验收正在执行:返回**成功结果**(不是 error,避免宿主把它当失败再重试放大)。 */
|
|
@@ -878,6 +889,8 @@ async function executeVerify(ctx, input) {
|
|
|
878
889
|
extraChecks: input.extraChecks,
|
|
879
890
|
checksMode: args.checksMode ?? "append",
|
|
880
891
|
projectVerify: input.projectVerify,
|
|
892
|
+
// issue #20:verify_task 的临时覆盖(三级继承最高优先级),仅本次验收生效
|
|
893
|
+
acceptanceOverride: args.acceptanceOverride,
|
|
881
894
|
baseline,
|
|
882
895
|
store,
|
|
883
896
|
logger,
|
|
@@ -180,6 +180,7 @@ export class TaskManager {
|
|
|
180
180
|
autoVerify: input.autoVerify,
|
|
181
181
|
autoFixRounds: input.autoFixRounds,
|
|
182
182
|
taskTimeoutMs: input.taskTimeoutMs,
|
|
183
|
+
acceptanceOverride: input.acceptanceOverride,
|
|
183
184
|
idempotencyKey: input.idempotencyKey,
|
|
184
185
|
idempotencyScope: input.idempotencyScope,
|
|
185
186
|
idempotencyDigest: input.idempotencyDigest,
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
|
-
import {
|
|
10
|
+
import { readAcceptanceLayer } from "../visual/config.js";
|
|
11
|
+
import { resolveAcceptanceLayers, summarizeResolved, } from "../config/acceptance-merge.js";
|
|
11
12
|
import { visualError } from "../visual/errors.js";
|
|
12
13
|
import { runVisual } from "../visual/engine.js";
|
|
13
14
|
import { checkVisualSnapshot, freezeVisualSnapshot } from "../visual/snapshot.js";
|
|
@@ -112,13 +113,28 @@ export class AcceptanceEngine {
|
|
|
112
113
|
const out = [];
|
|
113
114
|
const mode = req.checksMode ?? "append";
|
|
114
115
|
const hasExtra = !!req.extraChecks?.length;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
// ---- issue #20:三级继承(低 → 高)----
|
|
117
|
+
// 全局层落在数据目录(不在项目内),故此处自行推导 home,而不依赖 DataHome 实例。
|
|
118
|
+
const home = path.dirname(path.dirname(req.store.dir(req.taskId)));
|
|
119
|
+
const globalPath = path.join(home, "acceptance.default.json");
|
|
120
|
+
const projectPath = path.join(req.projectPath, ".tianshu-mcp", "acceptance.json");
|
|
121
|
+
const layers = [
|
|
122
|
+
{ source: "global", path: globalPath, config: (await readAcceptanceLayer(globalPath, "global")) ?? undefined },
|
|
123
|
+
{ source: "project", path: projectPath, config: (await readAcceptanceLayer(projectPath, "project")) ?? undefined },
|
|
124
|
+
{ source: "override", path: "(调用参数)", config: req.acceptanceOverride },
|
|
125
|
+
];
|
|
126
|
+
const resolved = resolveAcceptanceLayers(layers);
|
|
127
|
+
const merged = resolved.config;
|
|
128
|
+
// 每轮一行摘要:调用方与排障者无需额外命令即可看到「哪几层生效、最终取值」
|
|
129
|
+
this.logger.info(`验收配置层 task=${req.taskId} ${summarizeResolved(resolved)}`);
|
|
130
|
+
const requireChanges = merged.requireChanges ?? true;
|
|
131
|
+
const inProjectChecks = merged.checks?.map((c) => toAcceptanceDef(c));
|
|
132
|
+
// 先取基础集(合并后配置/默认),除非 replace 模式只用 extraChecks
|
|
118
133
|
if (!(mode === "replace" && hasExtra)) {
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
134
|
+
if (inProjectChecks !== undefined) {
|
|
135
|
+
const from = resolved.applied.map((a) => a.source).join("+");
|
|
136
|
+
notes.push(`使用验收配置层的 checks(生效层 ${from},共 ${inProjectChecks.length} 项)。`);
|
|
137
|
+
out.push(...inProjectChecks);
|
|
122
138
|
}
|
|
123
139
|
else if (req.projectVerify?.length) {
|
|
124
140
|
notes.push(`使用 server 数据目录 projects.json 补录的验收配置(${req.projectVerify.length} 项)。`);
|
|
@@ -141,16 +157,13 @@ export class AcceptanceEngine {
|
|
|
141
157
|
}
|
|
142
158
|
out.push(...(req.extraChecks ?? []));
|
|
143
159
|
}
|
|
144
|
-
return { checks: out, notes, requireChanges, verifyConcurrency: inProject?.verifyConcurrency };
|
|
145
|
-
}
|
|
146
|
-
async readProjectAcceptance(projectPath) {
|
|
147
|
-
const config = await readAcceptanceConfig(projectPath);
|
|
148
|
-
if (!config)
|
|
149
|
-
return null;
|
|
150
160
|
return {
|
|
151
|
-
checks:
|
|
152
|
-
|
|
153
|
-
|
|
161
|
+
checks: out,
|
|
162
|
+
notes,
|
|
163
|
+
requireChanges,
|
|
164
|
+
verifyConcurrency: merged.verifyConcurrency,
|
|
165
|
+
visual: merged.visual,
|
|
166
|
+
layers: resolved,
|
|
154
167
|
};
|
|
155
168
|
}
|
|
156
169
|
/** 执行一轮完整验收。返回 report + 是否 pass。 */
|
|
@@ -202,9 +215,11 @@ export class AcceptanceEngine {
|
|
|
202
215
|
notes: [error.message],
|
|
203
216
|
requireChanges: false,
|
|
204
217
|
verifyConcurrency: undefined,
|
|
218
|
+
visual: undefined,
|
|
219
|
+
layers: { config: {}, applied: [] },
|
|
205
220
|
};
|
|
206
221
|
});
|
|
207
|
-
const { checks: rawChecks, notes, requireChanges, verifyConcurrency } = resolved;
|
|
222
|
+
const { checks: rawChecks, notes, requireChanges, verifyConcurrency, layers } = resolved;
|
|
208
223
|
let visual;
|
|
209
224
|
if (!configurationError) {
|
|
210
225
|
try {
|
|
@@ -265,7 +280,9 @@ export class AcceptanceEngine {
|
|
|
265
280
|
try {
|
|
266
281
|
const frozen = await freezeVisualSnapshot(req.projectPath, req.store.dir(req.taskId));
|
|
267
282
|
await checkVisualSnapshot(req.projectPath, frozen);
|
|
268
|
-
|
|
283
|
+
// issue #20:visual 取三级合并结果,不再二次读取项目文件(否则 override/全局层的
|
|
284
|
+
// visual 会被项目文件的存在与否悄悄改变语义)。
|
|
285
|
+
const visualConfig = layers.config.visual;
|
|
269
286
|
if (visualConfig?.enabled)
|
|
270
287
|
visual = await runVisual(visualConfig, req.projectPath, path.dirname(path.dirname(req.store.dir(req.taskId))), path.join(req.store.dir(req.taskId), "visual", String(req.round)), req.signal);
|
|
271
288
|
await checkVisualSnapshot(req.projectPath, frozen);
|
package/dist/visual/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { AcceptanceConfigSchema } from "../config/schema.js";
|
|
3
|
+
import { AcceptanceConfigSchema, PartialAcceptanceConfigSchema, } from "../config/schema.js";
|
|
4
4
|
import { VisualError } from "./errors.js";
|
|
5
5
|
/** Only ENOENT means absent. Invalid or unreadable configuration never falls back. */
|
|
6
6
|
export async function readAcceptanceConfig(projectPath) {
|
|
@@ -20,3 +20,29 @@ export async function readAcceptanceConfig(projectPath) {
|
|
|
20
20
|
throw new VisualError("CONFIG_INVALID", `Invalid acceptance.json: ${String(e)}`);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* 读取**单层**验收配置(issue #20)。与 `readAcceptanceConfig` 的区别是:
|
|
25
|
+
* 1. 接受任意绝对路径(全局层是 `<数据目录>/acceptance.default.json`,不在项目内);
|
|
26
|
+
* 2. 用**无默认值**的 `PartialAcceptanceConfigSchema` 解析 —— 带默认值的 schema 会把
|
|
27
|
+
* `requireChanges: true` 之类凭空 materialize 出来,进而覆盖低优先级层的显式取值。
|
|
28
|
+
*
|
|
29
|
+
* 错误语义与项目级保持一致(fail-closed):仅 `ENOENT` 视为「该层不存在」,
|
|
30
|
+
* 读不了 → `CONFIG_UNREADABLE`,写坏/字段不合法 → `CONFIG_INVALID`。
|
|
31
|
+
*/
|
|
32
|
+
export async function readAcceptanceLayer(absPath, label) {
|
|
33
|
+
let text;
|
|
34
|
+
try {
|
|
35
|
+
text = await fs.readFile(absPath, "utf8");
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
if (e.code === "ENOENT")
|
|
39
|
+
return null;
|
|
40
|
+
throw new VisualError("CONFIG_UNREADABLE", `无法读取验收配置层 ${label}(${absPath}):${String(e)}`);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
return PartialAcceptanceConfigSchema.parse(JSON.parse(text));
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
throw new VisualError("CONFIG_INVALID", `验收配置层 ${label} 不合法(${absPath}):${String(e)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|