add-coder 0.3.33 → 0.3.35
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/dist/index.js +21 -3
- package/package.json +3 -3
- package/templates/.add-coder-src-hash.json +95 -41
- package/templates/adapters/claude/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/claude/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/claude/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/claude/hooks/session-start.mjs +65 -1
- package/templates/adapters/codex/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/codex/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/codex/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/codex/hooks/session-start.mjs +65 -1
- package/templates/adapters/qoder/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/qoder/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/qoder/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/qoder/hooks/session-start.mjs +67 -1
- package/templates/adapters/trae/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/trae/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/trae/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/trae/hooks/session-start.mjs +65 -1
- package/templates/adapters/vscode/hooks/doc-format-guard.mjs +172 -84
- package/templates/adapters/vscode/hooks/post-tool-use.mjs +59 -1
- package/templates/adapters/vscode/hooks/prompt-submit.mjs +72 -0
- package/templates/adapters/vscode/hooks/session-start.mjs +65 -1
- package/templates/core/governance/doc-format-guard.ts +29 -112
- package/templates/core/governance/post-tool-router.ts +33 -1
- package/templates/core/governance/prompt-router.ts +47 -0
- package/templates/core/governance/session-start-guard.ts +48 -1
- package/templates/core/prisma/add.prisma +203 -0
- package/templates/core/scripts/db-ensure.sh +61 -2
- package/templates/core/scripts/mcp-server/shared/db-types.ts +119 -0
- package/templates/core/scripts/mcp-server/shared/hitl-create-policy.ts +27 -0
- package/templates/core/scripts/mcp-server/shared/hitl-proposal-content.ts +110 -0
- package/templates/core/scripts/mcp-server/shared/hitl-widget-instance.ts +85 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/batch-fit.ts +250 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/feedback-stats.ts +101 -0
- package/templates/core/scripts/mcp-server/shared/memory/calibration/unit-state.ts +224 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/conflicts.ts +59 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/dedup.ts +45 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/errors.ts +33 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/handoff-digest.ts +92 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/metric-candidate.ts +79 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/scope.ts +94 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/secrets.ts +50 -0
- package/templates/core/scripts/mcp-server/shared/memory/domain/state-machine.ts +90 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/index.ts +117 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/local-onnx.ts +105 -0
- package/templates/core/scripts/mcp-server/shared/memory/embedding/openai-compatible.ts +87 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/consolidation.ts +226 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/evidence-collector.ts +153 -0
- package/templates/core/scripts/mcp-server/shared/memory/jobs/snapshot.ts +114 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-recall.ts +134 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-writer.ts +217 -0
- package/templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts +69 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts +89 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts +139 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +29 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts +106 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fusion.ts +43 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/pipeline.ts +285 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/query-terms.ts +31 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/recall-writer.ts +87 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/reranker.ts +116 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/types.ts +52 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/pgvector.ts +143 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/sqlite-vec.ts +118 -0
- package/templates/core/scripts/mcp-server/shared/memory/switches.ts +39 -0
- package/templates/core/scripts/mcp-server/shared/review-files.ts +22 -0
- package/templates/core/scripts/mcp-server/shared/runtime-freshness.ts +235 -0
- package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +41 -3
- package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +40 -1
- package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +2 -2
- package/templates/core/scripts/mcp-server/tools/hitl.ts +108 -42
- package/templates/core/scripts/mcp-server/tools/index.ts +7 -1
- package/templates/core/scripts/mcp-server/tools/memory-compat.ts +258 -0
- package/templates/core/scripts/mcp-server/tools/memory.ts +654 -0
- package/templates/core/scripts/mcp-server/tools/plan.ts +8 -3
- package/templates/core/scripts/mcp-server/tools/review.ts +10 -7
- package/templates/core/scripts/mcp-server.ts +36 -0
- package/templates/core/templates/checklist-template.md +13 -0
- package/templates/core/templates/review-implementation-template.md +24 -0
- package/templates/core/templates/review-template.md +16 -0
- package/templates/core/validation/index.ts +136 -0
- package/templates/core/validation/policy.ts +91 -0
- package/templates/core/validation/registry.ts +61 -0
- package/templates/core/validation/schema-validator.ts +277 -0
- package/templates/core/validation/validators/add-route.ts +32 -0
- package/templates/core/validation/validators/checklist.ts +48 -0
- package/templates/core/validation/validators/handoff.ts +46 -0
- package/templates/core/validation/validators/hitl.ts +22 -0
- package/templates/core/validation/validators/index.ts +52 -0
- package/templates/core/validation/validators/plan.ts +20 -0
- package/templates/core/validation/validators/report.ts +16 -0
- package/templates/core/validation/validators/review.ts +30 -0
- package/templates/core/validation/validators/spec.ts +25 -0
- package/templates/core/validation/validators/tasks.ts +40 -0
- package/templates/core/validation/validators/types.ts +32 -0
- package/templates/core/vocabulary/add-governance-vocabulary.md +18 -0
- package/templates/core/scripts/mcp-server/shared/dps-spec-ref.ts +0 -17
|
@@ -8,6 +8,8 @@ import { prisma } from "../shared/prisma.js"
|
|
|
8
8
|
import type { PlanRow, ReviewRow } from "../shared/db-types.js"
|
|
9
9
|
import { PlanRowSchema, ReviewRowSchema, validatedDelegate } from "../shared/db-types.js"
|
|
10
10
|
import { getRuntimeContext } from "../shared/env.js"
|
|
11
|
+
// 审查文档命名识别(纯函数,见 shared/review-files.ts:两种命名并存,规则需被用例钉住)
|
|
12
|
+
import { pickReviewFiles, derivePlanNameFromReviewFile } from "../shared/review-files.js"
|
|
11
13
|
import { assertPathInRuntimeScope } from "../shared/runtime-context.js"
|
|
12
14
|
|
|
13
15
|
const db = {
|
|
@@ -52,18 +54,19 @@ export function registerReviewTools(server: ToolRegistrar) {
|
|
|
52
54
|
const reviewsDir = join(PROJECT_ROOT, MAGIC_DIR, "reviews")
|
|
53
55
|
if (!existsSync(reviewsDir)) return errorResponse(`reviews 目录不存在: ${reviewsDir}`)
|
|
54
56
|
const allFiles = await readdirRecursive(reviewsDir)
|
|
55
|
-
|
|
57
|
+
/*
|
|
58
|
+
* 审查文档命名兼容(2026-09-14 放宽):
|
|
59
|
+
* 仓库里同时存在两种形态 —— `{planPrefix}-review-v1.md`(历史)与
|
|
60
|
+
* `{planPrefix}-plan-v1-review.md`(create_hitl 的占位路径落点,也是 review 文档的自然命名)。
|
|
61
|
+
* 旧匹配要求包含 `-review-`(review 后必须跟连字符),会把后者整类漏掉。
|
|
62
|
+
*/
|
|
63
|
+
const reviewFiles = pickReviewFiles(allFiles)
|
|
56
64
|
const results: string[] = []
|
|
57
65
|
let count = 0
|
|
58
66
|
|
|
59
67
|
for (const rf of reviewFiles) {
|
|
60
68
|
const fullPath = join(reviewsDir, rf)
|
|
61
|
-
|
|
62
|
-
// 格式: {plan-name}-review-v1.md 或 {plan-name}-review-implementation.md
|
|
63
|
-
const derivedPlan = basename(rf, ".md")
|
|
64
|
-
.replace(/-review-.*$/, "")
|
|
65
|
-
.replace(/-implementation.*$/, "")
|
|
66
|
-
.replace(/-runtime.*$/, "")
|
|
69
|
+
const derivedPlan = derivePlanNameFromReviewFile(rf)
|
|
67
70
|
// [2026-08-09 修复] 过滤方向颠倒:derivedPlan 是前缀,pn 是完整 planName({prefix}-plan-v{n}),
|
|
68
71
|
// 应为 pn.includes(derivedPlan);原实现 derivedPlan.includes(pn) 永远 false → 传入 planName 时全部跳过
|
|
69
72
|
if (pn && !pn.includes(derivedPlan)) continue
|
|
@@ -12,6 +12,40 @@ import {
|
|
|
12
12
|
} from "./mcp-server/shared/plan-lifecycle-subscriber.js"
|
|
13
13
|
import { PlanRoundSubscriber } from "./mcp-server/shared/plan-round-subscriber.js"
|
|
14
14
|
import { queryPlanRounds, type PlanRoundReadClient } from "./mcp-server/shared/plan-round-store.js"
|
|
15
|
+
import { PROJECT_ROOT, MAGIC_DIR } from "./mcp-server/shared/env.js"
|
|
16
|
+
import { clearRestartRequiredMarker } from "./mcp-server/shared/runtime-freshness.js"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 启动后清掉本 adapter 的"需重启"标记(2026-09-14):
|
|
20
|
+
* 该标记的语义是"运行中的 server 早于产物",本进程刚启动 → 必然晚于现有产物,
|
|
21
|
+
* 旧标记已失效。不清掉会留下**过期告警**(实测:重启后标记仍在,谁读到都以为还得重启)。
|
|
22
|
+
*/
|
|
23
|
+
function clearOwnStaleMarker(): void {
|
|
24
|
+
try {
|
|
25
|
+
clearRestartRequiredMarker(MAGIC_DIR, PROJECT_ROOT)
|
|
26
|
+
} catch {
|
|
27
|
+
/* fail-open:标记清理失败不影响启动 */
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 孤儿自退(2026-09-14):IDE/app 退出时通常只杀直接子进程(`npx`),
|
|
33
|
+
* `npm exec → sh -c → tsx → node` 这几代会被 reparent 到 systemd 继续存活
|
|
34
|
+
* (实测同一逻辑 server 5 个进程,旧实例的 server 在 app 重启后仍活 20+ 分钟;
|
|
35
|
+
* `.codex` 与 `.qoder` 上都反复出现)。
|
|
36
|
+
* 记录启动时的父 pid,一旦被 reparent 即视为启动者已死 → 自杀退出(stdio 对端本就没了)。
|
|
37
|
+
*/
|
|
38
|
+
function watchLauncher(): void {
|
|
39
|
+
const launcherPid = process.ppid
|
|
40
|
+
const timer = setInterval(() => {
|
|
41
|
+
if (process.ppid !== launcherPid) {
|
|
42
|
+
console.error(`[ADD-MCP] 启动者已退出(ppid ${launcherPid} → ${process.ppid}),孤儿自退`)
|
|
43
|
+
clearInterval(timer)
|
|
44
|
+
process.exit(0)
|
|
45
|
+
}
|
|
46
|
+
}, 15_000)
|
|
47
|
+
timer.unref?.()
|
|
48
|
+
}
|
|
15
49
|
|
|
16
50
|
async function main() {
|
|
17
51
|
const server = new McpServer(
|
|
@@ -21,6 +55,8 @@ async function main() {
|
|
|
21
55
|
registerAll(server)
|
|
22
56
|
const transport = new StdioServerTransport()
|
|
23
57
|
await server.connect(transport)
|
|
58
|
+
clearOwnStaleMarker()
|
|
59
|
+
watchLauncher()
|
|
24
60
|
let lifecycleSubscriber: PlanLifecycleSubscriber | undefined
|
|
25
61
|
let planRoundSubscriber: PlanRoundSubscriber | undefined
|
|
26
62
|
if (/^postgres(ql)?:\/\//.test(DATABASE_URL)) {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
- [ ] [T] 检查项1 — 证据: (待填写)|审计: (初验从 record_dev_operation 返回值写入真实 cuid,**禁止写 `cmq...` 占位符**)
|
|
15
15
|
- [ ] [T] 检查项2 — 证据: (待填写)|审计: (初验从 record_dev_operation 返回值写入真实 cuid,**禁止写 `cmq...` 占位符**)
|
|
16
|
+
- [ ] [T] schema 变更后运行实例 client 新鲜度(`db:generate` + 重启校验)— 证据: (待填写)|审计: (待填写) [来源: farm-agent 9737ab4 回灌;Prisma+Atlas 项目通用]
|
|
16
17
|
|
|
17
18
|
## ADD 规则合规检查
|
|
18
19
|
|
|
@@ -29,10 +30,22 @@
|
|
|
29
30
|
> `[T]` = 编译期可验证(AI 可在代码环节直接检查)
|
|
30
31
|
> `[R]` = 运行时验证(需部署运行后才能确认,自动流转到 review-runtime.md)
|
|
31
32
|
|
|
33
|
+
### 三通道验收(涉及图/节点/流式呈现时必做)
|
|
34
|
+
|
|
35
|
+
> [来源: farm-agent 9737ab4 回灌;与 review/review-implementation 的「三通道矩阵」配套,缺一即失败]
|
|
36
|
+
|
|
37
|
+
- [T] 逐节点三通道矩阵闭环:①生命周期 ②内容 ③结构化,缺一即失败 — 证据: 对照 Plan 三通道矩阵
|
|
38
|
+
- [T] 服务端未写死用户文案(文案在前端 i18n;服务端只发结构化字段/内容片段)— 证据: diff + grep
|
|
39
|
+
- [T] 终态渲染不覆盖运行期累计内容 — 证据: 实机/单测
|
|
40
|
+
- [T] 空态可渲染(0 条数据仍显示同构 DOM 或明确降级文案)— 证据: 实机截图/DOM 断言
|
|
41
|
+
- [T] 耗时刷新有事件源(服务端心跳或前端 ticker,二者已定稿其一)— 证据: 契约/实现 diff
|
|
42
|
+
- [R] 实机逐节点核对:运行中可见内容/结构化数据,终态内容不消失 — 证据: (待填写)
|
|
43
|
+
|
|
32
44
|
### 格式契约
|
|
33
45
|
|
|
34
46
|
- [T] 所有跨系统 API:发送方参数类型 = 接收方解析类型,字段名一致
|
|
35
47
|
- [T] 响应 Content-Type 匹配客户端解析器(JSON vs `text/event-stream` vs NDJSON)
|
|
48
|
+
- [T] 外部契约版本漂移:外部契约(如 API collection)更新后 diff 契约并对齐 client(详见 review-implementation §6.1)
|
|
36
49
|
|
|
37
50
|
### 框架版本
|
|
38
51
|
|
|
@@ -24,6 +24,20 @@
|
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
+
## 0. 三通道矩阵(涉及图/节点/流式呈现的变更强制)
|
|
28
|
+
|
|
29
|
+
> **规则**:实现审查逐节点核对 ①生命周期流 ②内容流 ③结构化流;缺任一 = P0。
|
|
30
|
+
> 与 Plan/Spec 的逐节点矩阵**逐项对齐**,并给出代码落点与实测证据。
|
|
31
|
+
> [来源: farm-agent 9737ab4 回灌;域字段已泛化为占位符]
|
|
32
|
+
|
|
33
|
+
| # | 节点 | action | ①生命周期流 | ②内容流 | ③结构化流 | 证据 |
|
|
34
|
+
|---|------|--------|:---:|------|------|------|
|
|
35
|
+
| 1 | {节点名} | {action 名} | {埋点/包装函数落点} | {内容承载} | {结构化字段投影} | {命令/抓包} |
|
|
36
|
+
|
|
37
|
+
- [ ] 三通道逐节点闭环(N/N,与 Plan 矩阵一致)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
27
41
|
## 1. 跨仓库格式契约
|
|
28
42
|
|
|
29
43
|
列出所有跨系统 API,逐对验证请求/响应格式:
|
|
@@ -74,6 +88,16 @@
|
|
|
74
88
|
|
|
75
89
|
---
|
|
76
90
|
|
|
91
|
+
## 6.1 流式端点结构化字段投影完整性
|
|
92
|
+
|
|
93
|
+
> [来源: farm-agent 9737ab4 回灌;原为具体域字段(reasoningPath/evidenceChain 等),此处泛化为契约表述]
|
|
94
|
+
|
|
95
|
+
- [ ] GUI 消费者必须覆盖流式事件(如 `{结构化事件名}`)的**全部业务字段**,禁止按最小可用裁剪
|
|
96
|
+
- [ ] 字段投影契约(spec `{契约章节}`)与前端消息字段 / UI 槽位**双向一致**(可用 `check_spec_sync` 校验)
|
|
97
|
+
- [ ] 外部契约版本漂移:外部契约(如 API collection)更新后 **diff 契约 + 对齐 client**
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
77
101
|
## 7. 关联 Checklist
|
|
78
102
|
|
|
79
103
|
- 本 review 的检查项与 `{checklist 文档路径}` 的"跨项目联调检查"章节一一对应
|
|
@@ -25,6 +25,22 @@
|
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
+
## 0. 三通道矩阵(涉及图/节点/流式呈现的变更强制)
|
|
29
|
+
|
|
30
|
+
> **规则**:逐节点核对 ①生命周期流 ②内容流 ③结构化流;**缺任一通道 = P0,不得通过 Review**。
|
|
31
|
+
> 「生命周期自动可观测」≠「内容存在」——内容/结构化数据必须由节点**显式发出**。
|
|
32
|
+
> [来源: farm-agent 9737ab4 回灌;域字段已泛化为占位符]
|
|
33
|
+
|
|
34
|
+
| # | 节点 | action | ①生命周期流 | ②内容流 | ③结构化流 | 证据/落点 |
|
|
35
|
+
|---|------|--------|:---:|------|------|---------|
|
|
36
|
+
| 1 | {节点名} | {action 名} | ✅/❌ | {内容载体(token/文本片段/其他)} | {结构化事件名 + 字段} | {文件/事件} |
|
|
37
|
+
|
|
38
|
+
- [ ] 每个节点三通道均已定义(无「只有状态没有内容」的节点)
|
|
39
|
+
- [ ] 空态(0 条数据 / 降级)仍有 DOM 或明确降级文案
|
|
40
|
+
- [ ] 耗时/进度刷新有事件源(心跳或前端 ticker,二者已定稿其一)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
28
44
|
## 1. 问题复现
|
|
29
45
|
|
|
30
46
|
为什么需要这次评审?
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 校验层单一入口(Plan core-validation-lifecycle Task 1.3 / Spec §4 §LifecycleWiring)
|
|
3
|
+
*
|
|
4
|
+
* **所有调用方(hook 卡位 / 收尾 / 批量命令 / 封口判定)都调这一个入口**:
|
|
5
|
+
* validate({ type, path, hook, mode?, expectRounds?, projectRoot, magicDir })
|
|
6
|
+
*
|
|
7
|
+
* 入口职责:读文档 → 解析类型配置(工厂)→ 取 schema 真源 → 执行 schema 校验 → 应用策略(口径)。
|
|
8
|
+
* 不在本层做任何"该类型特有"的判定(那是 validators/* 的专司)。
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
11
|
+
import { join } from "node:path"
|
|
12
|
+
import { resolveValidator } from "./registry.js"
|
|
13
|
+
import { applyApplicability, decidePolicy, type GovernanceHook, type ValidationMode } from "./policy.js"
|
|
14
|
+
import { typeCheckFor } from "./validators/index.js"
|
|
15
|
+
import {
|
|
16
|
+
validateAgainstSchema,
|
|
17
|
+
type SchemaFile,
|
|
18
|
+
type ValidationIssue,
|
|
19
|
+
} from "./schema-validator.js"
|
|
20
|
+
|
|
21
|
+
export interface ValidateInput {
|
|
22
|
+
type: string
|
|
23
|
+
/** 文档绝对路径 */
|
|
24
|
+
path: string
|
|
25
|
+
hook: GovernanceHook
|
|
26
|
+
/** 显式覆盖策略(缺省按卡位) */
|
|
27
|
+
mode?: ValidationMode
|
|
28
|
+
/** 多轮文档的期望轮次(由调用方给出,不由本层猜) */
|
|
29
|
+
expectRounds?: number
|
|
30
|
+
projectRoot: string
|
|
31
|
+
magicDir: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ValidateOutcome {
|
|
35
|
+
ok: boolean
|
|
36
|
+
type: string
|
|
37
|
+
hook: GovernanceHook
|
|
38
|
+
mode: ValidationMode
|
|
39
|
+
/** 策略依据(可审计) */
|
|
40
|
+
basis: string
|
|
41
|
+
schemaPath: string | null
|
|
42
|
+
/** 该卡位算作缺陷的问题(参与 ok 判定) */
|
|
43
|
+
issues: ValidationIssue[]
|
|
44
|
+
/** 该卡位不算缺陷、但仍需可见的问题(诊断;不参与 ok 判定)——规则适用性过滤的产物 */
|
|
45
|
+
diagnostics: ValidationIssue[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** schema 查找顺序:magic 目录副本优先,其次 templates/core 真源 */
|
|
49
|
+
function resolveSchemaPath(input: ValidateInput, schemaFile: string): string | null {
|
|
50
|
+
const candidates = [
|
|
51
|
+
join(input.projectRoot, input.magicDir, "templates", schemaFile),
|
|
52
|
+
join(input.projectRoot, "templates", "core", "templates", schemaFile),
|
|
53
|
+
]
|
|
54
|
+
return candidates.find((p) => existsSync(p)) ?? null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 模板查找(anchor 校验需要从模板对应行提取 token;顺序同 schema) */
|
|
58
|
+
function resolveTemplatePath(input: ValidateInput, templateFile: string): string | null {
|
|
59
|
+
const candidates = [
|
|
60
|
+
join(input.projectRoot, input.magicDir, "templates", templateFile),
|
|
61
|
+
join(input.projectRoot, "templates", "core", "templates", templateFile),
|
|
62
|
+
]
|
|
63
|
+
return candidates.find((p) => existsSync(p)) ?? null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function validate(input: ValidateInput): ValidateOutcome {
|
|
67
|
+
const config = resolveValidator(input.type) // 未注册类型 → 抛错(不回落通用校验)
|
|
68
|
+
const policy = decidePolicy(input.hook, input.type, input.mode)
|
|
69
|
+
const schemaPath = resolveSchemaPath(input, config.schema)
|
|
70
|
+
|
|
71
|
+
const base: Omit<ValidateOutcome, "ok" | "issues" | "diagnostics" | "schemaPath"> = {
|
|
72
|
+
type: input.type,
|
|
73
|
+
hook: input.hook,
|
|
74
|
+
mode: policy.mode,
|
|
75
|
+
basis: policy.basis,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// schema 缺失 = 无法判定 → 显式失败(禁止静默放行,Spec §1)
|
|
79
|
+
if (!schemaPath) {
|
|
80
|
+
return {
|
|
81
|
+
...base,
|
|
82
|
+
ok: false,
|
|
83
|
+
schemaPath: null,
|
|
84
|
+
issues: [
|
|
85
|
+
{
|
|
86
|
+
code: "MISSING_SECTION",
|
|
87
|
+
detail: `schema 真源缺失:${config.schema}(请执行 add-coder sync 或核对 templates/core/templates/)`,
|
|
88
|
+
expected: config.schema,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
diagnostics: [],
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!existsSync(input.path)) {
|
|
95
|
+
return {
|
|
96
|
+
...base,
|
|
97
|
+
ok: false,
|
|
98
|
+
schemaPath,
|
|
99
|
+
issues: [{ code: "MISSING_SECTION", detail: `文档不存在:${input.path}`, expected: input.path }],
|
|
100
|
+
diagnostics: [],
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const schema = JSON.parse(readFileSync(schemaPath, "utf-8")) as SchemaFile
|
|
105
|
+
const content = readFileSync(input.path, "utf-8")
|
|
106
|
+
// 锚点规则需要模板内容(token 从模板行提取);缺模板则锚点规则自动跳过(不误判)
|
|
107
|
+
const templatePath = schema.template ? resolveTemplatePath(input, schema.template) : null
|
|
108
|
+
const templateContent = templatePath ? readFileSync(templatePath, "utf-8") : undefined
|
|
109
|
+
// schema 层(形式)+ 专司层(本类型特有语义)——两层各自负责,互不重复
|
|
110
|
+
const rawIssues = validateAgainstSchema(content, schema, {
|
|
111
|
+
expectRounds: config.multiRound ? input.expectRounds : undefined,
|
|
112
|
+
templateContent,
|
|
113
|
+
})
|
|
114
|
+
const typeCheck = typeCheckFor(input.type)
|
|
115
|
+
if (typeCheck) {
|
|
116
|
+
rawIssues.push(...typeCheck({ type: input.type, content, path: input.path, expectRounds: input.expectRounds }))
|
|
117
|
+
}
|
|
118
|
+
// 规则适用性(Rule × Hook):不在本卡位生效的规则降级为诊断,不参与 ok 判定
|
|
119
|
+
const { applicable, diagnostics } = applyApplicability(rawIssues, input.hook)
|
|
120
|
+
return { ...base, ok: applicable.length === 0, schemaPath, issues: applicable, diagnostics }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { resolveValidator, VALIDATOR_REGISTRY, UnknownValidatorTypeError } from "./registry.js"
|
|
124
|
+
export { decidePolicy, applyApplicability, DEFAULT_RULE_APPLICABILITY } from "./policy.js"
|
|
125
|
+
export type { RuleApplicability, ApplicabilityResult } from "./policy.js"
|
|
126
|
+
export type { ValidationMode, GovernanceHook } from "./policy.js"
|
|
127
|
+
export {
|
|
128
|
+
validateAgainstSchema,
|
|
129
|
+
inferRoundHeading,
|
|
130
|
+
countRounds,
|
|
131
|
+
stripFencedBlocks,
|
|
132
|
+
normalizeWidth,
|
|
133
|
+
} from "./schema-validator.js"
|
|
134
|
+
export type { SchemaFile, SchemaSection, ValidationIssue, IssueCode } from "./schema-validator.js"
|
|
135
|
+
export { typeCheckFor, uncoveredTypes } from "./validators/index.js"
|
|
136
|
+
export type { TypeCheck, TypeCheckContext } from "./validators/types.js"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 校验策略(Plan core-validation-lifecycle Task 1.2 / Spec §3 §Policy)
|
|
3
|
+
*
|
|
4
|
+
* 口径由**卡位 + 文档类型**共同决定,且结果必须携带 mode 与依据(可审计)。
|
|
5
|
+
* 卡位编号沿用 `templates/core/docs/ADD-governance-*.md` 的 ADD 治理卡位 ①–⑭。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type ValidationMode = "advisory" | "blocking"
|
|
9
|
+
export type GovernanceHook =
|
|
10
|
+
| "SessionStart" | "UserPromptSubmit" | "PreToolUse" | "PostToolUse" | "PostToolUseFailure"
|
|
11
|
+
| "Stop" | "StopFailure" | "SessionEnd" | "PreCompact" | "SubagentStart" | "SubagentStop"
|
|
12
|
+
| "Notification" | "PermissionRequest" | "PermissionDenied" | "ConfigChange"
|
|
13
|
+
| "WorktreeCreate" | "WorktreeRemove" | "manual"
|
|
14
|
+
|
|
15
|
+
export interface PolicyDecision {
|
|
16
|
+
mode: ValidationMode
|
|
17
|
+
/** 依据说明(进入结果,可审计) */
|
|
18
|
+
basis: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 卡位默认口径(Spec §4 矩阵) */
|
|
22
|
+
const HOOK_DEFAULT: Partial<Record<GovernanceHook, { mode: ValidationMode; basis: string }>> = {
|
|
23
|
+
PreToolUse: { mode: "blocking", basis: "④ 写入前置守卫:预检阻断,避免写出不合规文档" },
|
|
24
|
+
PostToolUse: { mode: "advisory", basis: "⑤ 文档守卫:写后复检不阻断,转告警 + 留痕" },
|
|
25
|
+
Stop: { mode: "blocking", basis: "⑦ 验收检查:跨文档一致性与封口判定,阻断未收敛" },
|
|
26
|
+
SubagentStop: { mode: "blocking", basis: "⑪ 子代理结果校验:产物同样须合规" },
|
|
27
|
+
SessionEnd: { mode: "advisory", basis: "② 审计结算:批量结果入库,不阻断" },
|
|
28
|
+
manual: { mode: "advisory", basis: "手工/批量调用:仅报告,不阻断" },
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function decidePolicy(hook: GovernanceHook, type: string, override?: ValidationMode): PolicyDecision {
|
|
32
|
+
if (override) {
|
|
33
|
+
return { mode: override, basis: `调用方显式指定 mode=${override}(type=${type})` }
|
|
34
|
+
}
|
|
35
|
+
const byHook = HOOK_DEFAULT[hook]
|
|
36
|
+
if (byHook) return { mode: byHook.mode, basis: byHook.basis }
|
|
37
|
+
return { mode: "advisory", basis: `卡位 ${hook} 未声明口径 → 默认 advisory(不阻断)` }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* ───────────────────────── 规则适用性(Rule × Hook) ─────────────────────────
|
|
41
|
+
* 为什么需要(2026-09-13 修订:抽层时发现的范围放大):
|
|
42
|
+
* 规则集中到 core 后,若不加适用性约束,**原本只在某个卡位生效的规则会在所有卡位生效**——
|
|
43
|
+
* 例:锚定规则原为「写入时防劣化」,搬进 core 后会在收尾批量校验里把**历史文档**判为缺陷,
|
|
44
|
+
* 把噪音当违规。故规则集(what)与适用性(where)必须分开,严重度(how)由 decidePolicy 决定。
|
|
45
|
+
* ───────────────────────────────────────────────────────────────────────── */
|
|
46
|
+
|
|
47
|
+
import type { ValidationIssue } from "./schema-validator.js"
|
|
48
|
+
|
|
49
|
+
/** 规则码 → 适用卡位;`"*"` 表示全卡位 */
|
|
50
|
+
export type RuleApplicability = Readonly<Record<string, readonly GovernanceHook[] | "*">>
|
|
51
|
+
|
|
52
|
+
export const DEFAULT_RULE_APPLICABILITY: RuleApplicability = {
|
|
53
|
+
// 锚定类:仅"写入时防劣化"场景生效;批量/收尾不据此判历史文档为缺陷
|
|
54
|
+
ANCHOR_MISS: ["PreToolUse", "SubagentStop"],
|
|
55
|
+
// 缺失类:全卡位有效(文档缺失章节在哪都算缺陷)
|
|
56
|
+
MISSING_SECTION: "*",
|
|
57
|
+
MISSING_SUBSECTION: "*",
|
|
58
|
+
ROUND_COUNT_SHORT: "*",
|
|
59
|
+
// 占位符残留:全卡位有效
|
|
60
|
+
PLACEHOLDER_LEFT: "*",
|
|
61
|
+
// 结构位禁词:全卡位有效(0.3.27 语义)
|
|
62
|
+
FORBIDDEN_TERM: "*",
|
|
63
|
+
// checklist 证据占位:仅收尾/封口相关卡位算缺陷,写入时属"进行中"(advisory 诊断)
|
|
64
|
+
EVIDENCE_PLACEHOLDER_LEFT: ["Stop", "SessionEnd", "manual"],
|
|
65
|
+
} as const
|
|
66
|
+
|
|
67
|
+
export interface ApplicabilityResult {
|
|
68
|
+
/** 在该卡位算作缺陷的问题(参与 ok 判定) */
|
|
69
|
+
applicable: ValidationIssue[]
|
|
70
|
+
/** 在该卡位不算缺陷、但仍需可见的问题(诊断;不参与 ok 判定) */
|
|
71
|
+
diagnostics: ValidationIssue[]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 按卡位过滤问题:未声明适用性的规则码默认**全卡位适用**(保守:新规则不会因漏登记而静默失效)。
|
|
76
|
+
*/
|
|
77
|
+
export function applyApplicability(
|
|
78
|
+
issues: readonly ValidationIssue[],
|
|
79
|
+
hook: GovernanceHook,
|
|
80
|
+
applicability: RuleApplicability = DEFAULT_RULE_APPLICABILITY,
|
|
81
|
+
): ApplicabilityResult {
|
|
82
|
+
const applicable: ValidationIssue[] = []
|
|
83
|
+
const diagnostics: ValidationIssue[] = []
|
|
84
|
+
for (const issue of issues) {
|
|
85
|
+
const scope = applicability[issue.code]
|
|
86
|
+
const inScope = scope === undefined || scope === "*" || scope.includes(hook)
|
|
87
|
+
if (inScope) applicable.push(issue)
|
|
88
|
+
else diagnostics.push(issue)
|
|
89
|
+
}
|
|
90
|
+
return { applicable, diagnostics }
|
|
91
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* validator 注册表(Plan core-validation-lifecycle Task 1.2 / Spec §2 §ValidatorRegistry)
|
|
3
|
+
*
|
|
4
|
+
* 文档类型 → validator 配置的**工厂**。未注册类型直接报错——不回落"通用校验",
|
|
5
|
+
* 否则遗漏一个类型不会被发现(Spec §2)。
|
|
6
|
+
*
|
|
7
|
+
* 17 类文档与 schema 真源的对应关系见 Spec §5 覆盖表;schema 文件名由本表推导,不散落在调用方。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface ValidatorConfig {
|
|
11
|
+
/** 文档类型标识(与 Spec §5 覆盖表一一对应) */
|
|
12
|
+
type: string
|
|
13
|
+
/** schema 文件名(位于 `{magicDir}/templates/` 或 `templates/core/templates/`) */
|
|
14
|
+
schema: string
|
|
15
|
+
/** 是否多轮文档(需要 expectRounds) */
|
|
16
|
+
multiRound?: boolean
|
|
17
|
+
/** 该类型特有的附加规则标识(由 validators/* 实现消费) */
|
|
18
|
+
extra?: "checklistT" | "tasksCheckbox" | "addRouteSteps"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const VALIDATOR_REGISTRY: readonly ValidatorConfig[] = [
|
|
22
|
+
{ type: "plan.standard", schema: "standard-plan-template.schema.json" },
|
|
23
|
+
{ type: "plan.simple", schema: "simple-plan-template.schema.json" },
|
|
24
|
+
{ type: "spec", schema: "spec-template.schema.json" },
|
|
25
|
+
{ type: "tasks", schema: "tasks-template.schema.json", extra: "tasksCheckbox" },
|
|
26
|
+
{ type: "checklist", schema: "checklist-template.schema.json", extra: "checklistT" },
|
|
27
|
+
{ type: "add-route", schema: "add-route-template.schema.json", extra: "addRouteSteps" },
|
|
28
|
+
{ type: "handoff.single", schema: "handoff-single-round-template.schema.json" },
|
|
29
|
+
{ type: "handoff.multi", schema: "handoff-multi-round-template.schema.json", multiRound: true },
|
|
30
|
+
{ type: "review", schema: "review-template.schema.json" },
|
|
31
|
+
{ type: "review.implementation", schema: "review-implementation-template.schema.json" },
|
|
32
|
+
{ type: "review.runtime", schema: "review-runtime-template.schema.json" },
|
|
33
|
+
{ type: "hitl", schema: "hitl-template.schema.json" },
|
|
34
|
+
{ type: "report", schema: "report-template.schema.json" },
|
|
35
|
+
{ type: "runtime-report", schema: "runtime-report-template.schema.json" },
|
|
36
|
+
{ type: "collab-contract", schema: "collab-contract-template.schema.json" },
|
|
37
|
+
{ type: "fix-verification", schema: "fix-verification-template.schema.json" },
|
|
38
|
+
{ type: "prd", schema: "prd-standard-template.schema.json" },
|
|
39
|
+
] as const
|
|
40
|
+
|
|
41
|
+
export class UnknownValidatorTypeError extends Error {
|
|
42
|
+
constructor(type: string) {
|
|
43
|
+
super(
|
|
44
|
+
`未注册的文档类型:${type}。已注册:${VALIDATOR_REGISTRY.map((v) => v.type).join(", ")}`,
|
|
45
|
+
)
|
|
46
|
+
this.name = "UnknownValidatorTypeError"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 工厂:按类型取配置;未注册即抛(不回落通用校验) */
|
|
51
|
+
export function resolveValidator(type: string): ValidatorConfig {
|
|
52
|
+
const found = VALIDATOR_REGISTRY.find((v) => v.type === type)
|
|
53
|
+
if (!found) throw new UnknownValidatorTypeError(type)
|
|
54
|
+
return found
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 覆盖完整性自检:返回 schema 清单中未被注册的类型对应文件(供 checklist 断言用) */
|
|
58
|
+
export function unregisteredSchemas(schemaFiles: readonly string[]): string[] {
|
|
59
|
+
const registered = new Set(VALIDATOR_REGISTRY.map((v) => v.schema))
|
|
60
|
+
return schemaFiles.filter((f) => !registered.has(f))
|
|
61
|
+
}
|