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.
Files changed (98) hide show
  1. package/dist/index.js +21 -3
  2. package/package.json +3 -3
  3. package/templates/.add-coder-src-hash.json +95 -41
  4. package/templates/adapters/claude/hooks/doc-format-guard.mjs +172 -84
  5. package/templates/adapters/claude/hooks/post-tool-use.mjs +59 -1
  6. package/templates/adapters/claude/hooks/prompt-submit.mjs +72 -0
  7. package/templates/adapters/claude/hooks/session-start.mjs +65 -1
  8. package/templates/adapters/codex/hooks/doc-format-guard.mjs +172 -84
  9. package/templates/adapters/codex/hooks/post-tool-use.mjs +59 -1
  10. package/templates/adapters/codex/hooks/prompt-submit.mjs +72 -0
  11. package/templates/adapters/codex/hooks/session-start.mjs +65 -1
  12. package/templates/adapters/qoder/hooks/doc-format-guard.mjs +172 -84
  13. package/templates/adapters/qoder/hooks/post-tool-use.mjs +59 -1
  14. package/templates/adapters/qoder/hooks/prompt-submit.mjs +72 -0
  15. package/templates/adapters/qoder/hooks/session-start.mjs +67 -1
  16. package/templates/adapters/trae/hooks/doc-format-guard.mjs +172 -84
  17. package/templates/adapters/trae/hooks/post-tool-use.mjs +59 -1
  18. package/templates/adapters/trae/hooks/prompt-submit.mjs +72 -0
  19. package/templates/adapters/trae/hooks/session-start.mjs +65 -1
  20. package/templates/adapters/vscode/hooks/doc-format-guard.mjs +172 -84
  21. package/templates/adapters/vscode/hooks/post-tool-use.mjs +59 -1
  22. package/templates/adapters/vscode/hooks/prompt-submit.mjs +72 -0
  23. package/templates/adapters/vscode/hooks/session-start.mjs +65 -1
  24. package/templates/core/governance/doc-format-guard.ts +29 -112
  25. package/templates/core/governance/post-tool-router.ts +33 -1
  26. package/templates/core/governance/prompt-router.ts +47 -0
  27. package/templates/core/governance/session-start-guard.ts +48 -1
  28. package/templates/core/prisma/add.prisma +203 -0
  29. package/templates/core/scripts/db-ensure.sh +61 -2
  30. package/templates/core/scripts/mcp-server/shared/db-types.ts +119 -0
  31. package/templates/core/scripts/mcp-server/shared/hitl-create-policy.ts +27 -0
  32. package/templates/core/scripts/mcp-server/shared/hitl-proposal-content.ts +110 -0
  33. package/templates/core/scripts/mcp-server/shared/hitl-widget-instance.ts +85 -0
  34. package/templates/core/scripts/mcp-server/shared/memory/calibration/batch-fit.ts +250 -0
  35. package/templates/core/scripts/mcp-server/shared/memory/calibration/feedback-stats.ts +101 -0
  36. package/templates/core/scripts/mcp-server/shared/memory/calibration/unit-state.ts +224 -0
  37. package/templates/core/scripts/mcp-server/shared/memory/domain/conflicts.ts +59 -0
  38. package/templates/core/scripts/mcp-server/shared/memory/domain/dedup.ts +45 -0
  39. package/templates/core/scripts/mcp-server/shared/memory/domain/errors.ts +33 -0
  40. package/templates/core/scripts/mcp-server/shared/memory/domain/handoff-digest.ts +92 -0
  41. package/templates/core/scripts/mcp-server/shared/memory/domain/metric-candidate.ts +79 -0
  42. package/templates/core/scripts/mcp-server/shared/memory/domain/scope.ts +94 -0
  43. package/templates/core/scripts/mcp-server/shared/memory/domain/secrets.ts +50 -0
  44. package/templates/core/scripts/mcp-server/shared/memory/domain/state-machine.ts +90 -0
  45. package/templates/core/scripts/mcp-server/shared/memory/embedding/index.ts +117 -0
  46. package/templates/core/scripts/mcp-server/shared/memory/embedding/local-onnx.ts +105 -0
  47. package/templates/core/scripts/mcp-server/shared/memory/embedding/openai-compatible.ts +87 -0
  48. package/templates/core/scripts/mcp-server/shared/memory/jobs/consolidation.ts +226 -0
  49. package/templates/core/scripts/mcp-server/shared/memory/jobs/evidence-collector.ts +153 -0
  50. package/templates/core/scripts/mcp-server/shared/memory/jobs/snapshot.ts +114 -0
  51. package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-recall.ts +134 -0
  52. package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-writer.ts +217 -0
  53. package/templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts +69 -0
  54. package/templates/core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts +89 -0
  55. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts +139 -0
  56. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +29 -0
  57. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts +106 -0
  58. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fusion.ts +43 -0
  59. package/templates/core/scripts/mcp-server/shared/memory/retrieval/pipeline.ts +285 -0
  60. package/templates/core/scripts/mcp-server/shared/memory/retrieval/query-terms.ts +31 -0
  61. package/templates/core/scripts/mcp-server/shared/memory/retrieval/recall-writer.ts +87 -0
  62. package/templates/core/scripts/mcp-server/shared/memory/retrieval/reranker.ts +116 -0
  63. package/templates/core/scripts/mcp-server/shared/memory/retrieval/types.ts +52 -0
  64. package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/pgvector.ts +143 -0
  65. package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/sqlite-vec.ts +118 -0
  66. package/templates/core/scripts/mcp-server/shared/memory/switches.ts +39 -0
  67. package/templates/core/scripts/mcp-server/shared/review-files.ts +22 -0
  68. package/templates/core/scripts/mcp-server/shared/runtime-freshness.ts +235 -0
  69. package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +41 -3
  70. package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +40 -1
  71. package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +2 -2
  72. package/templates/core/scripts/mcp-server/tools/hitl.ts +108 -42
  73. package/templates/core/scripts/mcp-server/tools/index.ts +7 -1
  74. package/templates/core/scripts/mcp-server/tools/memory-compat.ts +258 -0
  75. package/templates/core/scripts/mcp-server/tools/memory.ts +654 -0
  76. package/templates/core/scripts/mcp-server/tools/plan.ts +8 -3
  77. package/templates/core/scripts/mcp-server/tools/review.ts +10 -7
  78. package/templates/core/scripts/mcp-server.ts +36 -0
  79. package/templates/core/templates/checklist-template.md +13 -0
  80. package/templates/core/templates/review-implementation-template.md +24 -0
  81. package/templates/core/templates/review-template.md +16 -0
  82. package/templates/core/validation/index.ts +136 -0
  83. package/templates/core/validation/policy.ts +91 -0
  84. package/templates/core/validation/registry.ts +61 -0
  85. package/templates/core/validation/schema-validator.ts +277 -0
  86. package/templates/core/validation/validators/add-route.ts +32 -0
  87. package/templates/core/validation/validators/checklist.ts +48 -0
  88. package/templates/core/validation/validators/handoff.ts +46 -0
  89. package/templates/core/validation/validators/hitl.ts +22 -0
  90. package/templates/core/validation/validators/index.ts +52 -0
  91. package/templates/core/validation/validators/plan.ts +20 -0
  92. package/templates/core/validation/validators/report.ts +16 -0
  93. package/templates/core/validation/validators/review.ts +30 -0
  94. package/templates/core/validation/validators/spec.ts +25 -0
  95. package/templates/core/validation/validators/tasks.ts +40 -0
  96. package/templates/core/validation/validators/types.ts +32 -0
  97. package/templates/core/vocabulary/add-governance-vocabulary.md +18 -0
  98. package/templates/core/scripts/mcp-server/shared/dps-spec-ref.ts +0 -17
@@ -0,0 +1,277 @@
1
+ /*
2
+ * schema 驱动校验器(Plan core-validation-lifecycle Task 1.1 / Spec §1 §SchemaSource)
3
+ *
4
+ * **唯一真源**:全部形式判定读 `templates/core/templates/*.schema.json`。
5
+ * 本文件内**不得出现任何硬编码章节名**——章节、子章节、允许占位符、禁词全部来自 schema。
6
+ *
7
+ * 校验项:
8
+ * - required section 缺失 → MISSING_SECTION
9
+ * - section 的 subsections 缺失 → MISSING_SUBSECTION
10
+ * - 多轮文档轮次不足 → ROUND_COUNT_SHORT(由调用方给 expectRounds)
11
+ * - schema.placeholders 中仍残留的占位符 → PLACEHOLDER_LEFT
12
+ * - schema.forbidden_terms 命中(**围栏代码块外**)→ FORBIDDEN_TERM
13
+ *
14
+ * 匹配口径:**半角/全角等价**(见 normalizeWidth)。schema 真源只用一种写法,
15
+ * 判定时对两侧同时归一,避免"冒号全角了就判缺章节"这类假缺陷。
16
+ */
17
+
18
+ export interface SchemaSection {
19
+ id: string
20
+ heading?: string
21
+ required?: boolean
22
+ subsections?: { heading: string }[]
23
+ /** 语义锚定:在模板对应行中提取 token,要求这些 token 出现在文档(可配合 within 限定范围) */
24
+ anchor?: string
25
+ within?: string
26
+ }
27
+
28
+ export interface SchemaFile {
29
+ template: string
30
+ sections: SchemaSection[]
31
+ placeholders?: string[]
32
+ forbidden_terms?: string[]
33
+ /** 结构位列号(1-based,含首列分隔符偏移,与既有守卫语义一致) */
34
+ groupColumn?: number | string
35
+ }
36
+
37
+ /** schema 层(形式)检查码 */
38
+ export type SchemaIssueCode =
39
+ | "MISSING_SECTION"
40
+ | "MISSING_SUBSECTION"
41
+ | "ROUND_COUNT_SHORT"
42
+ | "PLACEHOLDER_LEFT"
43
+ | "FORBIDDEN_TERM"
44
+
45
+ /**
46
+ * 问题码:schema 层用固定枚举;类型特有规则(validators/*)可声明自己的码
47
+ * (如 `AUDIT_QUERY_MISSING`、`UNVERIFIED_T_LEFT`),故开放为字符串联合。
48
+ */
49
+ export type IssueCode = SchemaIssueCode | (string & {})
50
+
51
+ export interface ValidationIssue {
52
+ code: IssueCode
53
+ detail: string
54
+ expected?: string
55
+ }
56
+
57
+ export interface ValidateOptions {
58
+ /** 多轮文档的期望轮次数(来自 Plan/执行记录,不由本模块猜) */
59
+ expectRounds?: number
60
+ /** 轮次标题的模板串(默认取 schema 中 heading 含 `<第N轮>` 的 section) */
61
+ roundHeading?: string
62
+ /** 模板内容(anchor 校验需要:token 从模板对应行提取) */
63
+ templateContent?: string
64
+ }
65
+
66
+ /** 统计子串出现次数 */
67
+ function countOccurrences(haystack: string, needle: string): number {
68
+ if (!needle) return 0
69
+ const h = normalizeWidth(haystack)
70
+ const n = normalizeWidth(needle)
71
+ let count = 0
72
+ let idx = h.indexOf(n)
73
+ while (idx !== -1) {
74
+ count++
75
+ idx = h.indexOf(n, idx + n.length)
76
+ }
77
+ return count
78
+ }
79
+
80
+ /**
81
+ * 半角/全角归一(2026-09-14:半角全角都支持)。
82
+ *
83
+ * 为什么放在**匹配层**而不是 schema 真源:真源是形式定义,不该为书写习惯准备两份;
84
+ * 归一化属于"怎么比",与"比什么"分离(同 policy.ts 的规则/适用性分离)。
85
+ *
86
+ * 覆盖:全角 ASCII 区 U+FF01–U+FF5E → U+0021–U+007E(:()?!,;/# + 全角字母数字),
87
+ * 以及表意空格 U+3000 → 半角空格。两侧同时归一,故任一侧写全角都能匹配。
88
+ * 逐码点 1:1 映射,归一后字符串长度不变——indexOf 的下标语义不受影响。
89
+ */
90
+ const FULLWIDTH_START = 0xff01
91
+ const FULLWIDTH_END = 0xff5e
92
+ const FULLWIDTH_TO_HALF = 0xfee0
93
+ const WIDTH_FOLD_EXTRA: Readonly<Record<string, string>> = { "\u3000": " " }
94
+
95
+ export function normalizeWidth(text: string): string {
96
+ let out = ""
97
+ for (const ch of text) {
98
+ const code = ch.codePointAt(0) as number
99
+ if (code >= FULLWIDTH_START && code <= FULLWIDTH_END) {
100
+ out += String.fromCharCode(code - FULLWIDTH_TO_HALF)
101
+ } else {
102
+ out += WIDTH_FOLD_EXTRA[ch] ?? ch
103
+ }
104
+ }
105
+ return out
106
+ }
107
+
108
+ /** 去掉围栏代码块(禁词判定不该命中示例代码与引用块) */
109
+ export function stripFencedBlocks(content: string): string {
110
+ return content.replace(/```[\s\S]*?```/g, "\n")
111
+ }
112
+
113
+ /**
114
+ * 结构位文本:**标题行 + groupColumn 列**(与既有 doc-format-guard 语义一致)。
115
+ * 禁词只在该范围内判定——正文正常叙述里出现"阶段"不构成违规。
116
+ */
117
+ export function structText(content: string, groupColumn?: number | string): string {
118
+ let text = (content.match(/^#{2,}\s.*$/gm) ?? []).join("\n")
119
+ const col = typeof groupColumn === "number" ? groupColumn : Number(groupColumn)
120
+ if (Number.isFinite(col) && col > 0) {
121
+ const cells = content
122
+ .split("\n")
123
+ .map((line) => {
124
+ const cells = line.split("|")
125
+ return cells.length > col ? (cells[col + 1] ?? "").trim() : ""
126
+ })
127
+ .filter(Boolean)
128
+ text += "\n" + cells.join("\n")
129
+ }
130
+ return text
131
+ }
132
+
133
+ /**
134
+ * 语义锚定校验(0.3.27 能力,上移至此以免抽层时丢失):
135
+ * 从模板中含 anchor 的行提取 token,要求它们出现在文档(`within` 存在时限定该节范围)。
136
+ * 模板行或 within 无法定位时**跳过该规则**(与既有守卫一致,不误判)。
137
+ */
138
+ export function validateAnchors(
139
+ content: string,
140
+ schema: SchemaFile,
141
+ templateContent: string,
142
+ ): ValidationIssue[] {
143
+ const issues: ValidationIssue[] = []
144
+ const nContent = normalizeWidth(content)
145
+ for (const section of schema.sections) {
146
+ if (!section.anchor) continue
147
+ const refAnchor = normalizeWidth(section.anchor)
148
+ const refLine = templateContent.split("\n").find((l) => normalizeWidth(l).includes(refAnchor))
149
+ if (!refLine) continue
150
+ const tokens = [
151
+ ...new Set(
152
+ normalizeWidth(refLine)
153
+ .replace(/[#*`|(){]/g, " ")
154
+ .split(/\s+/)
155
+ .filter((t) => t !== "" && !t.includes("{")),
156
+ ),
157
+ ]
158
+ if (tokens.length === 0) continue
159
+ let scope = nContent
160
+ if (section.within) {
161
+ const startIdx = nContent.indexOf(normalizeWidth(section.within))
162
+ if (startIdx < 0) continue
163
+ const endIdx = nContent.indexOf("\n## ", startIdx + 1)
164
+ scope = nContent.slice(startIdx, endIdx === -1 ? undefined : endIdx)
165
+ }
166
+ const missTokens = tokens.filter((tok) => !scope.includes(tok))
167
+ if (missTokens.length > 0) {
168
+ issues.push({
169
+ code: "ANCHOR_MISS",
170
+ detail: `缺锚点(${section.id}): ${missTokens.join(" ")}`,
171
+ expected: section.anchor,
172
+ })
173
+ }
174
+ }
175
+ return issues
176
+ }
177
+
178
+ /** 从 schema 推断轮次标题模板(`## <第N轮>` 这类) */
179
+ export function inferRoundHeading(schema: SchemaFile): string | null {
180
+ const round = schema.sections.find((s) => s.heading?.includes("<第N轮>"))
181
+ return round?.heading ?? null
182
+ }
183
+
184
+ /** 轮次标题在文档中的实际出现次数(把 `<第N轮>` 视为通配片段) */
185
+ export function countRounds(content: string, roundHeading: string): number {
186
+ const heading = normalizeWidth(roundHeading)
187
+ if (!heading.includes("<第N轮>")) return 0
188
+ /*
189
+ * `<第N轮>` → `第<数字>轮`(2026-09-14 修正):
190
+ * 旧实现把通配片段缩成「本行以轮结尾」,于是模板自己示范的写法
191
+ * `## <第N轮> {Round简短描述}`(落地即 `## 第 1 轮 抽层`)被计为 **0 轮** →
192
+ * 多轮 handoff 一律假报 ROUND_COUNT_SHORT。改为按"第+数字+轮"识别(只锚行首、不锚行尾):
193
+ * 既能计带描述的轮次标题,也不会把 `## 附录` / `## 轮次依赖` 之类误计为轮次。
194
+ */
195
+ const pattern = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace("<第N轮>", "第\\s*\\d+\\s*轮")
196
+ const re = new RegExp(`^${pattern}`, "gm") // 行首锚定即可,`{Round简短描述}` 可有可无
197
+ return (normalizeWidth(content).match(re) ?? []).length
198
+ }
199
+
200
+ export function validateAgainstSchema(
201
+ content: string,
202
+ schema: SchemaFile,
203
+ opts: ValidateOptions = {},
204
+ ): ValidationIssue[] {
205
+ const issues: ValidationIssue[] = []
206
+ const proseOnly = stripFencedBlocks(content)
207
+ /** 归一化视图(匹配用,报告仍引原文);口径与原文一致:占位符查全文(含围栏内),禁词查结构位 */
208
+ const contentNormalized = normalizeWidth(content)
209
+
210
+ // 1) 章节与子章节
211
+ const roundHeading = opts.roundHeading ?? inferRoundHeading(schema)
212
+ for (const section of schema.sections) {
213
+ if (!section.heading) continue
214
+ const isRoundSection = roundHeading !== null && section.heading === roundHeading
215
+ if (isRoundSection) {
216
+ // 轮次章节始终走轮次计数(默认至少 1 轮),不与字面 `<第N轮>` 比对
217
+ const need = opts.expectRounds ?? 1
218
+ const found = countRounds(content, roundHeading as string)
219
+ if (found < need) {
220
+ issues.push({
221
+ code: "ROUND_COUNT_SHORT",
222
+ detail: `轮次章节不足:期望 ${need} 轮,实际 ${found} 轮`,
223
+ expected: roundHeading,
224
+ })
225
+ }
226
+ // 轮次子章节(每轮一套)必须逐项检查——这是多轮文档的核心规格,不能被 continue 跳过
227
+ for (const sub of section.subsections ?? []) {
228
+ if (countOccurrences(content, sub.heading) < need) {
229
+ issues.push({
230
+ code: "MISSING_SUBSECTION",
231
+ detail: `轮次子章节缺失或不足(需要 ${need} 份):${sub.heading}`,
232
+ expected: sub.heading,
233
+ })
234
+ }
235
+ }
236
+ continue
237
+ }
238
+ if (section.required && countOccurrences(content, section.heading) === 0) {
239
+ issues.push({ code: "MISSING_SECTION", detail: `缺少必需章节:${section.heading}`, expected: section.heading })
240
+ }
241
+ for (const sub of section.subsections ?? []) {
242
+ const need = isRoundSection ? (opts.expectRounds ?? 1) : 1
243
+ if (countOccurrences(content, sub.heading) < need) {
244
+ issues.push({
245
+ code: "MISSING_SUBSECTION",
246
+ detail: `子章节缺失或不足(需要 ${need} 份):${sub.heading}`,
247
+ expected: sub.heading,
248
+ })
249
+ }
250
+ }
251
+ }
252
+
253
+ // 2) 占位符残留(只查 schema 声明的那些;半角/全角写法都算残留——放宽写法不等于放过残留)
254
+ for (const ph of schema.placeholders ?? []) {
255
+ if (!ph) continue
256
+ const nPh = normalizeWidth(ph)
257
+ const idx = contentNormalized.indexOf(nPh)
258
+ if (idx === -1) continue
259
+ const hit = nPh === ph ? ph : `${content.slice(idx, idx + nPh.length)}(宽度等价于 ${ph})`
260
+ issues.push({ code: "PLACEHOLDER_LEFT", detail: `占位符未替换:${hit}`, expected: ph })
261
+ }
262
+
263
+ // 3) 结构位禁词(**标题行 + groupColumn 列**;正文叙述不判)
264
+ const struct = normalizeWidth(structText(content, schema.groupColumn))
265
+ for (const term of schema.forbidden_terms ?? []) {
266
+ if (term && struct.includes(normalizeWidth(term))) {
267
+ issues.push({ code: "FORBIDDEN_TERM", detail: `结构位禁词:${term}(仅标题行与指定列判定)`, expected: term })
268
+ }
269
+ }
270
+
271
+ // 4) 语义锚定(需要模板内容;缺模板则跳过,不误判)
272
+ if (opts.templateContent) {
273
+ issues.push(...validateAnchors(content, schema, opts.templateContent))
274
+ }
275
+
276
+ return issues
277
+ }
@@ -0,0 +1,32 @@
1
+ /*
2
+ * add-route 专司(Task 2.3 / Spec §5)
3
+ *
4
+ * 语义规则:产出项统计(`- [ ]` / `- [x]`,与 `check_add_route_completeness` 同口径)
5
+ * 与「Task 映射表」存在性——add-route 是 Plan→代码的唯一映射表。
6
+ */
7
+ import { countMatches, issue, type TypeCheckContext } from "./types.js"
8
+ import type { ValidationIssue } from "../schema-validator.js"
9
+
10
+ export interface AddRouteStats {
11
+ total: number
12
+ open: number
13
+ done: number
14
+ }
15
+
16
+ export function addRouteStats(content: string): AddRouteStats {
17
+ const done = countMatches(content, /^- \[[xX]\]/gm)
18
+ const open = countMatches(content, /^- \[ \]/gm)
19
+ return { total: done + open, open, done }
20
+ }
21
+
22
+ export function checkAddRoute(ctx: TypeCheckContext): ValidationIssue[] {
23
+ const issues: ValidationIssue[] = []
24
+ const stats = addRouteStats(ctx.content)
25
+ if (stats.total === 0) {
26
+ issues.push(issue("ADD_ROUTE_CHECKBOX_MISSING", "未发现任何产出项复选框(- [ ] / - [x])"))
27
+ }
28
+ if (!/Task\s*映射表|Task 映射表/.test(ctx.content)) {
29
+ issues.push(issue("ADD_ROUTE_TASK_MAP_MISSING", "缺少 Task 映射表(Plan→代码的唯一映射)"))
30
+ }
31
+ return issues
32
+ }
@@ -0,0 +1,48 @@
1
+ /*
2
+ * checklist 专司(Task 2.2 / Spec §5)
3
+ *
4
+ * 语义规则(schema 不表达):
5
+ * - `[T]` 未勾选数(封口"验收证据"因子的输入);
6
+ * - `[R]` 项存在性(运行时验证须留有清单,不得为零);
7
+ * - 「(待填写)」证据占位残留(证据必须真实,不接受占位)。
8
+ */
9
+ import { countMatches, issue, type TypeCheckContext } from "./types.js"
10
+ import type { ValidationIssue } from "../schema-validator.js"
11
+
12
+ export interface ChecklistStats {
13
+ tTotal: number
14
+ tDone: number
15
+ tOpen: number
16
+ rTotal: number
17
+ evidencePlaceholders: number
18
+ }
19
+
20
+ export function checklistStats(content: string): ChecklistStats {
21
+ const tDone = countMatches(content, /^- \[[xX]\] \[T\]/gm)
22
+ const tOpen = countMatches(content, /^- \[ \] \[T\]/gm)
23
+ return {
24
+ tTotal: tDone + tOpen,
25
+ tDone,
26
+ tOpen,
27
+ rTotal: countMatches(content, /^- \[[ xX]\] \[R\]/gm),
28
+ evidencePlaceholders: countMatches(content, /\(待填写\)/gm),
29
+ }
30
+ }
31
+
32
+ export function checkChecklist(ctx: TypeCheckContext): ValidationIssue[] {
33
+ const issues: ValidationIssue[] = []
34
+ const s = checklistStats(ctx.content)
35
+
36
+ if (s.tTotal === 0) {
37
+ issues.push(issue("CHECKLIST_T_MISSING", "未发现任何 [T] 编译期验证项(checklist 缺少可判定内容)"))
38
+ }
39
+ if (s.rTotal === 0) {
40
+ issues.push(issue("CHECKLIST_R_MISSING", "未发现任何 [R] 运行时验证项(须留待运行时清单)"))
41
+ }
42
+ if (s.evidencePlaceholders > 0) {
43
+ issues.push(
44
+ issue("EVIDENCE_PLACEHOLDER_LEFT", `证据占位残留 ${s.evidencePlaceholders} 处((待填写))——证据必须为真实命令与结果`),
45
+ )
46
+ }
47
+ return issues
48
+ }
@@ -0,0 +1,46 @@
1
+ /*
2
+ * handoff 专司(Task 2.1 / Spec §2 多轮计数 + §4)
3
+ *
4
+ * schema 层已负责:章节/子章节存在性、轮次计数、占位符、禁词。
5
+ * 本专司只补两条 schema 表达不了的语义规则:
6
+ * 1. **§8 必须含可执行审计查询**(`query_audit_logs(`)——"给下一个会话的入口"不能只是文字描述;
7
+ * 2. **§9 后置确认必须是逐项勾选状态**(不能整节留空)。
8
+ */
9
+ import { countMatches, issue, proseOnly, type TypeCheckContext } from "./types.js"
10
+ import type { ValidationIssue } from "../schema-validator.js"
11
+
12
+ export function checkHandoff(ctx: TypeCheckContext): ValidationIssue[] {
13
+ const issues: ValidationIssue[] = []
14
+ const content = ctx.content
15
+
16
+ if (!/query_audit_logs\s*\(/.test(content)) {
17
+ issues.push(
18
+ issue("AUDIT_QUERY_MISSING", "缺少可执行审计查询(query_audit_logs(...))——下一会话无从恢复上下文"),
19
+ )
20
+ }
21
+
22
+ // 后置确认:取「后置确认」之后的所有复选框
23
+ const idx = content.indexOf("后置确认")
24
+ const post = idx >= 0 ? content.slice(idx) : ""
25
+ const total = countMatches(post, /^- \[[ xX]\]/gm)
26
+ if (idx < 0) {
27
+ issues.push(issue("POSTCHECK_MISSING", "缺少「后置确认」章节", "后置确认"))
28
+ } else if (total === 0) {
29
+ issues.push(issue("POSTCHECK_EMPTY", "后置确认无逐项勾选状态(须列明确认项)"))
30
+ }
31
+
32
+ // 多轮文档:每轮的验证标准必须可判定(非空、含可执行证据字样)
33
+ if ((ctx.expectRounds ?? 1) > 1) {
34
+ const verdicts = countMatches(content, /^- \[[ xX]\]/gm)
35
+ if (verdicts === 0) {
36
+ issues.push(issue("ROUND_VERDICT_MISSING", "多轮文档未见任何逐项验证标准(- [ ] / - [x])"))
37
+ }
38
+ }
39
+
40
+ // 脱敏:不得出现凭据硬编码(此处只做"可疑赋值"提示,属 advisory 语义)
41
+ if (/(POSTGRES_PASSWORD|JWT_SECRET|API_KEY)\s*[:=]\s*["'][^"']{8,}["']/.test(proseOnly(content))) {
42
+ issues.push(issue("SECRET_HARDCODED", "疑似凭据硬编码(应以环境变量引用)"))
43
+ }
44
+
45
+ return issues
46
+ }
@@ -0,0 +1,22 @@
1
+ /*
2
+ * hitl 专司(Task 2.4 / Spec §5)
3
+ *
4
+ * 语义规则:提案必须含维度表格且至少 1 行(无维度 = 无法逐项拍板);
5
+ * 且状态字段须存在(DRAFT/TONGYI/BOHUI 可判定)。
6
+ */
7
+ import { countMatches, issue, proseOnly, type TypeCheckContext } from "./types.js"
8
+ import type { ValidationIssue } from "../schema-validator.js"
9
+
10
+ export function checkHitl(ctx: TypeCheckContext): ValidationIssue[] {
11
+ const issues: ValidationIssue[] = []
12
+ const body = proseOnly(ctx.content)
13
+
14
+ const dimensionRows = countMatches(body, /^\|\s*\d+\s*\|[^|]+\|[^|]+\|/gm)
15
+ if (dimensionRows === 0) {
16
+ issues.push(issue("HITL_DIMENSION_MISSING", "提案无决策维度行(无法逐项拍板)"))
17
+ }
18
+ if (!/状态\s*[::]/.test(body)) {
19
+ issues.push(issue("HITL_STATUS_MISSING", "提案缺少状态字段(DRAFT/TONGYI/BOHUI)"))
20
+ }
21
+ return issues
22
+ }
@@ -0,0 +1,52 @@
1
+ /*
2
+ * 专司注册(Plan core-validation-lifecycle Task 2.5 / Spec §2 §ValidatorRegistry)
3
+ *
4
+ * 类型 → 专司检查函数。**未注册类型的专司缺口会被显式报告**(`uncoveredTypes()`),
5
+ * 避免"加了新文档类型但忘了写专司"这种事悄悄发生。
6
+ */
7
+ import type { TypeCheck } from "./types.js"
8
+ import { checkHandoff } from "./handoff.js"
9
+ import { checkChecklist } from "./checklist.js"
10
+ import { checkTasks } from "./tasks.js"
11
+ import { checkSpec } from "./spec.js"
12
+ import { checkPlan } from "./plan.js"
13
+ import { checkAddRoute } from "./add-route.js"
14
+ import { checkReview } from "./review.js"
15
+ import { checkHitl } from "./hitl.js"
16
+ import { checkReport } from "./report.js"
17
+
18
+ /** 类型 → 专司(可多对一:同类文档共用一份专司逻辑,但入口按 type 显式登记) */
19
+ export const TYPE_CHECKS: Readonly<Record<string, TypeCheck>> = {
20
+ "plan.standard": checkPlan,
21
+ "plan.simple": checkPlan,
22
+ spec: checkSpec,
23
+ tasks: checkTasks,
24
+ checklist: checkChecklist,
25
+ "add-route": checkAddRoute,
26
+ "handoff.single": checkHandoff,
27
+ "handoff.multi": checkHandoff,
28
+ review: checkReview,
29
+ "review.implementation": checkReview,
30
+ "review.runtime": checkReview,
31
+ hitl: checkHitl,
32
+ report: checkReport,
33
+ "runtime-report": checkReport,
34
+ "collab-contract": checkReport,
35
+ "fix-verification": checkReport,
36
+ prd: checkReport,
37
+ } as const
38
+
39
+ /** 取得该类型的专司(未登记返回 null,由调用方决定是否报缺口) */
40
+ export function typeCheckFor(type: string): TypeCheck | null {
41
+ return TYPE_CHECKS[type] ?? null
42
+ }
43
+
44
+ /** 覆盖自检:registry 里已注册但缺少专司的类型(供 checklist 断言) */
45
+ export function uncoveredTypes(registeredTypes: readonly string[]): string[] {
46
+ return registeredTypes.filter((t) => !(t in TYPE_CHECKS))
47
+ }
48
+
49
+ export { checkHandoff, checkChecklist, checkTasks, checkSpec, checkPlan, checkAddRoute, checkReview, checkHitl, checkReport }
50
+ export { checklistStats } from "./checklist.js"
51
+ export { tasksStats } from "./tasks.js"
52
+ export { addRouteStats } from "./add-route.js"
@@ -0,0 +1,20 @@
1
+ /*
2
+ * plan 专司(Task 2.3 / Spec §5)
3
+ *
4
+ * 语义规则:元信息必含「Plan 名称 / 启动时间 / 状态」三字段(HITL 与纳管依赖它们),
5
+ * 且状态不得停留在"待审批"以外的含糊表述(须可判定)。
6
+ */
7
+ import { issue, type TypeCheckContext } from "./types.js"
8
+ import type { ValidationIssue } from "../schema-validator.js"
9
+
10
+ const REQUIRED_META = ["**Plan 名称**", "**启动时间**", "**状态**"]
11
+
12
+ export function checkPlan(ctx: TypeCheckContext): ValidationIssue[] {
13
+ const issues: ValidationIssue[] = []
14
+ for (const field of REQUIRED_META) {
15
+ if (!ctx.content.includes(field)) {
16
+ issues.push(issue("PLAN_META_MISSING", `Plan 元信息缺少字段:${field}`, field))
17
+ }
18
+ }
19
+ return issues
20
+ }
@@ -0,0 +1,16 @@
1
+ /*
2
+ * report 类专司(Task 2.4 / Spec §5:report / runtime-report / collab-contract / fix-verification / prd)
3
+ *
4
+ * 语义规则:收尾类文档必须有明确结论段("结论/判定/建议"之一),否则只是过程叙述。
5
+ */
6
+ import { issue, proseOnly, type TypeCheckContext } from "./types.js"
7
+ import type { ValidationIssue } from "../schema-validator.js"
8
+
9
+ export function checkReport(ctx: TypeCheckContext): ValidationIssue[] {
10
+ const issues: ValidationIssue[] = []
11
+ const body = proseOnly(ctx.content)
12
+ if (!/结论|判定|建议|Triage 结果/.test(body)) {
13
+ issues.push(issue("REPORT_CONCLUSION_MISSING", "缺少结论/判定/建议段(收尾类文档须可判定)"))
14
+ }
15
+ return issues
16
+ }
@@ -0,0 +1,30 @@
1
+ /*
2
+ * review 专司(Task 2.4 / Spec §5,三型共用:方案 / 实现 / 运行时)
3
+ *
4
+ * 语义规则:必须有「问题清单」章节且清单条目带严重度标记(P0/P1/P2 或 🔴🟡🟢),
5
+ * 否则 Review 只是叙述、不构成可执行缺陷清单。
6
+ */
7
+ import { countMatches, issue, proseOnly, type TypeCheckContext } from "./types.js"
8
+ import type { ValidationIssue } from "../schema-validator.js"
9
+
10
+ export function checkReview(ctx: TypeCheckContext): ValidationIssue[] {
11
+ const issues: ValidationIssue[] = []
12
+ const body = proseOnly(ctx.content)
13
+
14
+ // 发现清单的等价形态:HITL 发现总览(review-template 的现行结构)/ 问题清单 / 发现列表
15
+ // (2026-09-13 修订:原实现只认后两者,对 review-template 产生假阳性)
16
+ if (!/HITL 发现总览|问题清单|发现列表/.test(body)) {
17
+ issues.push(issue("REVIEW_FINDINGS_MISSING", "缺少发现清单(HITL 发现总览 / 问题清单 / 发现列表)"))
18
+ }
19
+ const severities = countMatches(body, /(🔴|🟡|🟢)|\bP[012]\b/g)
20
+ if (severities === 0) {
21
+ issues.push(issue("REVIEW_SEVERITY_MISSING", "清单条目未见严重度标记(P0/P1/P2 或 🔴🟡🟢)"))
22
+ }
23
+ if (ctx.type === "review.runtime") {
24
+ const unchecked = countMatches(body, /^- \[ \]\s*Triage/gm)
25
+ if (unchecked > 0) {
26
+ issues.push(issue("RUNTIME_TRIAGE_OPEN", `运行时发现未 Triage ${unchecked} 条(不得静默累积)`))
27
+ }
28
+ }
29
+ return issues
30
+ }
@@ -0,0 +1,25 @@
1
+ /*
2
+ * spec 专司(Task 2.2 / Spec §5)
3
+ *
4
+ * 语义规则:`Plan→Spec 映射` 表至少 1 行(DPS 的映射锚定依赖它),
5
+ * 且每个 Requirement 至少含一条 WHEN-THEN(可判定性)。
6
+ */
7
+ import { countMatches, issue, proseOnly, type TypeCheckContext } from "./types.js"
8
+ import type { ValidationIssue } from "../schema-validator.js"
9
+
10
+ export function checkSpec(ctx: TypeCheckContext): ValidationIssue[] {
11
+ const issues: ValidationIssue[] = []
12
+ const body = proseOnly(ctx.content)
13
+
14
+ const mappingRows = countMatches(body, /^\|\s*\d+\s*\|.*\|/gm)
15
+ if (mappingRows === 0) {
16
+ issues.push(issue("PLAN_SPEC_MAP_MISSING", "缺少「Plan→Spec 映射」表(DPS 映射锚定依赖)"))
17
+ }
18
+
19
+ const requirements = countMatches(body, /^#{2,3}\s+\d+\.\s+/gm)
20
+ const whenThen = countMatches(body, /^\s*-\s*WHEN\s+/gm)
21
+ if (requirements > 0 && whenThen === 0) {
22
+ issues.push(issue("WHEN_THEN_MISSING", "存在 Requirements 章节但无任何 WHEN-THEN 条目(不可判定)"))
23
+ }
24
+ return issues
25
+ }
@@ -0,0 +1,40 @@
1
+ /*
2
+ * tasks 专司(Task 2.2 / Spec §5)
3
+ *
4
+ * 语义规则:Task 勾选统计(与 `plan_track` 的进度口径一致),
5
+ * 以及「Plan→Task 映射表」存在性(tasks.md 必须能对回 Plan 的 Task)。
6
+ */
7
+ import { countMatches, issue, type TypeCheckContext } from "./types.js"
8
+ import type { ValidationIssue } from "../schema-validator.js"
9
+
10
+ export interface TasksStats {
11
+ total: number
12
+ done: number
13
+ }
14
+
15
+ /** Task 级统计:`### Task N.M` 标题数与其中已完成(含子项全勾)的量 */
16
+ export function tasksStats(content: string): TasksStats {
17
+ const headings = [...content.matchAll(/^###\s+Task\s+\d+(?:\.\d+)?\s*[::]/gm)]
18
+ const total = headings.length
19
+ let done = 0
20
+ for (let i = 0; i < headings.length; i++) {
21
+ const start = headings[i].index ?? 0
22
+ const end = i + 1 < headings.length ? (headings[i + 1].index ?? content.length) : content.length
23
+ const body = content.slice(start, end)
24
+ const boxes = body.match(/^- \[[ xX]\]/gm) ?? []
25
+ if (boxes.length > 0 && boxes.every((b) => /\[[xX]\]/.test(b))) done++
26
+ }
27
+ return { total, done }
28
+ }
29
+
30
+ export function checkTasks(ctx: TypeCheckContext): ValidationIssue[] {
31
+ const issues: ValidationIssue[] = []
32
+ const stats = tasksStats(ctx.content)
33
+ if (stats.total === 0) {
34
+ issues.push(issue("TASK_HEADING_MISSING", "未发现 `### Task N.M:` 形式的 Task 标题(无法拆解执行)"))
35
+ }
36
+ if (!/Plan\s*→\s*Task|Plan→Task/.test(ctx.content)) {
37
+ issues.push(issue("PLAN_TASK_MAP_MISSING", "缺少「Plan→Task 映射」表(Tasks 必须可对回 Plan)"))
38
+ }
39
+ return issues
40
+ }
@@ -0,0 +1,32 @@
1
+ /*
2
+ * 专司 validator 的共享契约(Plan core-validation-lifecycle Task 2.x / Spec §2 §ValidatorRegistry)
3
+ *
4
+ * 分工原则:**schema 层管形式,专司层管本类型特有的语义**。
5
+ * 专司只补充「schema 表达不了」的规则(如"§8 必须有可执行审计查询"、"checklist 的 [T] 未勾数"),
6
+ * 不重复章节存在性判定——那由 schema 层负责(Spec §1)。
7
+ */
8
+ import type { ValidationIssue } from "../schema-validator.js"
9
+
10
+ export interface TypeCheckContext {
11
+ type: string
12
+ content: string
13
+ path: string
14
+ /** 多轮文档的期望轮次(由调用方给出) */
15
+ expectRounds?: number
16
+ }
17
+
18
+ export type TypeCheck = (ctx: TypeCheckContext) => ValidationIssue[]
19
+
20
+ export function issue(code: string, detail: string, expected?: string): ValidationIssue {
21
+ return expected === undefined ? { code, detail } : { code, detail, expected }
22
+ }
23
+
24
+ /** 统计匹配次数(供各专司复用) */
25
+ export function countMatches(content: string, re: RegExp): number {
26
+ return (content.match(re) ?? []).length
27
+ }
28
+
29
+ /** 去掉围栏代码块(语义检查一般不该被示例代码命中) */
30
+ export function proseOnly(content: string): string {
31
+ return content.replace(/```[\s\S]*?```/g, "\n")
32
+ }