spec-wave 2.1.1

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 (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +327 -0
  3. package/README.zh-CN.md +327 -0
  4. package/RELEASING.md +52 -0
  5. package/assets/README.md +30 -0
  6. package/assets/ci/samples/README.md +76 -0
  7. package/assets/ci/samples/hgm-ingest.yml.example +42 -0
  8. package/assets/ci/samples/lint-wiki-delta.pin.yml.example +47 -0
  9. package/assets/ci/samples/lint-wiki-delta.yml.example +49 -0
  10. package/assets/ci/samples/pytest.yml.example +49 -0
  11. package/assets/ci/samples/quality.yml.example +56 -0
  12. package/assets/ci/samples/skills-validate.yml.example +38 -0
  13. package/assets/ci/samples/tech-graph.yml.example +55 -0
  14. package/assets/coding_wiki/templates/README.md +115 -0
  15. package/assets/coding_wiki/templates/_index.md +19 -0
  16. package/assets/coding_wiki/templates/context.md +18 -0
  17. package/assets/coding_wiki/templates/stable.md +26 -0
  18. package/assets/coding_wiki/templates/topics/wiki_layout.md +13 -0
  19. package/assets/coding_wiki/templates/topics/wikilinks_export.md +13 -0
  20. package/assets/coding_wiki/templates/volatile.md +25 -0
  21. package/assets/docs/POINTER_ONBOARDING.md +9 -0
  22. package/assets/docs/POINTER_RUNBOOK_wiki_delta.md +18 -0
  23. package/assets/docs/POINTER_SDD_HAT_FLOW.md +8 -0
  24. package/assets/docs/POINTER_USER_GUIDE.md +8 -0
  25. package/assets/graph/stubs/README.md +6 -0
  26. package/assets/graph/templates/00_main.graph.yaml +72 -0
  27. package/assets/graph/templates/00_main.md +85 -0
  28. package/assets/graph/templates/01_struct.md +38 -0
  29. package/assets/graph/templates/02_version.md +7 -0
  30. package/assets/graph/templates/10_flow_MAIN.graph.yaml +105 -0
  31. package/assets/graph/templates/10_flow_MAIN.md +103 -0
  32. package/assets/graph/templates/99_mermaid_protocol.md +156 -0
  33. package/assets/graph/templates/README.md +66 -0
  34. package/assets/harness/README.md +18 -0
  35. package/assets/harness/discipline-coverage.yaml +320 -0
  36. package/assets/harness/invokes/README.md +22 -0
  37. package/assets/harness/invokes/TEMPLATE_invoke.md +61 -0
  38. package/assets/harness/lifecycle.yaml +127 -0
  39. package/assets/harness/prompts/00-orchestrator.md +91 -0
  40. package/assets/harness/prompts/10-spec-requirements.md +63 -0
  41. package/assets/harness/prompts/10-task-requirements.md +70 -0
  42. package/assets/harness/prompts/20-spec-audit.md +50 -0
  43. package/assets/harness/prompts/20-task-audit.md +90 -0
  44. package/assets/harness/prompts/30-execute-code.md +64 -0
  45. package/assets/harness/prompts/40-self-check.md +65 -0
  46. package/assets/harness/prompts/FRAGMENT_00_delegate_only_v1_zh.md +19 -0
  47. package/assets/harness/prompts/FRAGMENT_30_gate_verify_v1_zh.md +34 -0
  48. package/assets/harness/prompts/FRAGMENT_30_invoke_block_v1_zh.md +28 -0
  49. package/assets/harness/prompts/FRAGMENT_hat_reanchor_v1_zh.md +19 -0
  50. package/assets/harness/prompts/README.md +67 -0
  51. package/assets/harness/prompts/TEMPLATE_30_gate_stop.md +36 -0
  52. package/assets/harness/templates/FRAGMENT_task_domain_infer_v1_zh.md +16 -0
  53. package/assets/harness/templates/ONTOLOGY_consumer_slice_v1.md +41 -0
  54. package/assets/harness/templates/QUICKREF_v1_zh.md +35 -0
  55. package/assets/harness/templates/README.md +50 -0
  56. package/assets/harness/templates/TASK_TEMPLATE.md +133 -0
  57. package/assets/harness/templates/TASK_done_README.md +76 -0
  58. package/assets/harness/templates/TASK_epic.md +103 -0
  59. package/assets/harness/templates/TASK_graph_bootstrap.md +115 -0
  60. package/assets/harness/templates/VIEW_done_by_domain.md +66 -0
  61. package/assets/harness/templates/VIEW_done_thin_pointer.md +16 -0
  62. package/assets/ide/adapters/AGENTS.md.fragment.example +33 -0
  63. package/assets/ide/adapters/CLAUDE.md.fragment.example +32 -0
  64. package/assets/ide/adapters/README.md +57 -0
  65. package/assets/ide/adapters/cursor-harness-starter.mdc.example +39 -0
  66. package/assets/ide/commands/claude/kit/apply-standards.md +10 -0
  67. package/assets/ide/commands/claude/kit/gate-status.md +10 -0
  68. package/assets/ide/commands/claude/kit/graph-check.md +8 -0
  69. package/assets/ide/commands/claude/kit/hat-00-delegate.md +9 -0
  70. package/assets/ide/commands/claude/kit/hat-10-spec.md +9 -0
  71. package/assets/ide/commands/claude/kit/hat-10-task.md +9 -0
  72. package/assets/ide/commands/claude/kit/hat-20-spec-audit.md +9 -0
  73. package/assets/ide/commands/claude/kit/hat-20-task-audit.md +9 -0
  74. package/assets/ide/commands/claude/kit/hat-reanchor.md +8 -0
  75. package/assets/ide/commands/claude/kit/init-guide.md +16 -0
  76. package/assets/ide/commands/claude/kit/sync-prompts-guide.md +9 -0
  77. package/assets/ide/commands/claude/kit/verify.md +16 -0
  78. package/assets/ide/commands/cursor/kit-apply-standards.md +12 -0
  79. package/assets/ide/commands/cursor/kit-gate-status.md +12 -0
  80. package/assets/ide/commands/cursor/kit-graph-check.md +10 -0
  81. package/assets/ide/commands/cursor/kit-hat-00-delegate.md +11 -0
  82. package/assets/ide/commands/cursor/kit-hat-10-spec.md +11 -0
  83. package/assets/ide/commands/cursor/kit-hat-10-task.md +11 -0
  84. package/assets/ide/commands/cursor/kit-hat-20-spec-audit.md +11 -0
  85. package/assets/ide/commands/cursor/kit-hat-20-task-audit.md +11 -0
  86. package/assets/ide/commands/cursor/kit-hat-reanchor.md +10 -0
  87. package/assets/ide/commands/cursor/kit-init-guide.md +18 -0
  88. package/assets/ide/commands/cursor/kit-sync-prompts-guide.md +11 -0
  89. package/assets/ide/commands/cursor/kit-verify.md +18 -0
  90. package/assets/ide/host-adapt/README.md +113 -0
  91. package/assets/ide/host-adapt/examples/mvp-hosts.yaml +78 -0
  92. package/assets/ide/host-adapt/host-adapt.schema.json +97 -0
  93. package/assets/ide/skills-orch/README.md +16 -0
  94. package/assets/ide/skills-orch/kit-apply-standards/SKILL.md +17 -0
  95. package/assets/ide/skills-orch/kit-gate-status/SKILL.md +17 -0
  96. package/assets/ide/skills-orch/kit-hat-reanchor/SKILL.md +15 -0
  97. package/assets/ide/skills-orch/kit-init-guide/SKILL.md +23 -0
  98. package/assets/ide/skills-orch/kit-verify/SKILL.md +23 -0
  99. package/assets/ontology.yaml +113 -0
  100. package/assets/skills/README.md +37 -0
  101. package/assets/skills/harness-00-delegate-only/SKILL.md +19 -0
  102. package/assets/skills/harness-10-spec/SKILL.md +63 -0
  103. package/assets/skills/harness-10-task/SKILL.md +70 -0
  104. package/assets/skills/harness-20-spec-audit/SKILL.md +50 -0
  105. package/assets/skills/harness-20-task-audit/SKILL.md +90 -0
  106. package/assets/skills/harness-20-task-audit/references/TEMPLATE_30_gate_stop.md +36 -0
  107. package/assets/skills/harness-hat-reanchor/SKILL.md +19 -0
  108. package/assets/standards/POINTER_workspace_truth_v1_zh.md +42 -0
  109. package/assets/standards/README.md +35 -0
  110. package/assets/standards/SOURCES_v1_zh.md +49 -0
  111. package/assets/standards/TEMPLATE_CODING_BASELINE_L1_v1_zh.md +88 -0
  112. package/assets/standards/TEMPLATE_CODING_BASELINE_L2_backend_v1_zh.md +75 -0
  113. package/assets/standards/TEMPLATE_CODING_BASELINE_L2_frontend_v1_zh.md +76 -0
  114. package/bin/dsh-coding-kit.js +7 -0
  115. package/bin/specgate.js +7 -0
  116. package/bin/validate_template_compliance.py +470 -0
  117. package/cordis.patch.yml +5 -0
  118. package/lib/cli-checks.d.ts +68 -0
  119. package/lib/cli-checks.js +812 -0
  120. package/lib/cli-checks.js.map +1 -0
  121. package/lib/cli-graph-hgm.d.ts +74 -0
  122. package/lib/cli-graph-hgm.js +416 -0
  123. package/lib/cli-graph-hgm.js.map +1 -0
  124. package/lib/cli-graph-yaml.d.ts +79 -0
  125. package/lib/cli-graph-yaml.js +602 -0
  126. package/lib/cli-graph-yaml.js.map +1 -0
  127. package/lib/cli-graph.d.ts +1 -0
  128. package/lib/cli-graph.js +199 -0
  129. package/lib/cli-graph.js.map +1 -0
  130. package/lib/cli-host.d.ts +24 -0
  131. package/lib/cli-host.js +1197 -0
  132. package/lib/cli-host.js.map +1 -0
  133. package/lib/cli-lifecycle.d.ts +2 -0
  134. package/lib/cli-lifecycle.js +401 -0
  135. package/lib/cli-lifecycle.js.map +1 -0
  136. package/lib/cli-refresh-ide-blocks.d.ts +66 -0
  137. package/lib/cli-refresh-ide-blocks.js +484 -0
  138. package/lib/cli-refresh-ide-blocks.js.map +1 -0
  139. package/lib/cli-shared.d.ts +74 -0
  140. package/lib/cli-shared.js +281 -0
  141. package/lib/cli-shared.js.map +1 -0
  142. package/lib/cli-skills.d.ts +52 -0
  143. package/lib/cli-skills.js +363 -0
  144. package/lib/cli-skills.js.map +1 -0
  145. package/lib/cli-status.d.ts +2 -0
  146. package/lib/cli-status.js +251 -0
  147. package/lib/cli-status.js.map +1 -0
  148. package/lib/cli-sync-prompts.d.ts +25 -0
  149. package/lib/cli-sync-prompts.js +158 -0
  150. package/lib/cli-sync-prompts.js.map +1 -0
  151. package/lib/cli-sync.d.ts +2 -0
  152. package/lib/cli-sync.js +124 -0
  153. package/lib/cli-sync.js.map +1 -0
  154. package/lib/cli-task-extra.d.ts +53 -0
  155. package/lib/cli-task-extra.js +408 -0
  156. package/lib/cli-task-extra.js.map +1 -0
  157. package/lib/cli-timeline.d.ts +28 -0
  158. package/lib/cli-timeline.js +68 -0
  159. package/lib/cli-timeline.js.map +1 -0
  160. package/lib/cli-wiki.d.ts +21 -0
  161. package/lib/cli-wiki.js +189 -0
  162. package/lib/cli-wiki.js.map +1 -0
  163. package/lib/cli.d.ts +22 -0
  164. package/lib/cli.js +1034 -0
  165. package/lib/cli.js.map +1 -0
  166. package/lib/host-contract.d.ts +23 -0
  167. package/lib/host-contract.js +143 -0
  168. package/lib/host-contract.js.map +1 -0
  169. package/lib/index.d.ts +18 -0
  170. package/lib/index.js +290 -0
  171. package/lib/index.js.map +1 -0
  172. package/lib/yaml.d.ts +4 -0
  173. package/lib/yaml.js +10 -0
  174. package/lib/yaml.js.map +1 -0
  175. package/package.json +87 -0
@@ -0,0 +1,470 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ AICoding 架构专家团 · 模板合规自动校验器
5
+ =========================================
6
+
7
+ 用途:在 G1~G5 阶段门处校验 `.workbuddy/output/` 下的产物文档是否严格遵循
8
+ `aicoding-team-bootstrap/templates/` 中的模板章节骨架。
9
+
10
+ 用法:
11
+ python3 bin/validate_template_compliance.py --output-dir .workbuddy/output
12
+ python3 bin/validate_template_compliance.py --output-dir .workbuddy/output --filter 高层架构设计.md
13
+ python3 bin/validate_template_compliance.py --output-dir .workbuddy/output --json
14
+
15
+ 退出码:0 = 全部通过;1 = 存在 FAIL;2 = 参数/环境错误。
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import json
22
+ import os
23
+ import re
24
+ import sys
25
+ from typing import Dict, List, Tuple
26
+
27
+ # 模板目录:优先环境变量,其次专家包内标准位置
28
+ TEMPLATE_DIR_ENV = "AICODING_TEMPLATE_DIR"
29
+ DEFAULT_TEMPLATE_DIRS = [
30
+ os.path.expanduser(
31
+ "~/.workbuddy/plugins/cache/experts/aicoding-architecture-expert-team/1.0.0"
32
+ "/skills/aicoding-team-bootstrap/templates"
33
+ ),
34
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "templates"),
35
+ ]
36
+
37
+ # 产物文件名 -> 模板文件名(同名的可省略,此处显式声明以保证可追溯)
38
+ DOC_TEMPLATE_MAP = {
39
+ "material_digest.md": "material_digest.md",
40
+ "research_report.md": "research_report.md",
41
+ "高层架构设计.md": "高层架构设计.md",
42
+ "系统设计.md": "系统设计.md",
43
+ "UserStory.md": "UserStory.md",
44
+ "部署设计.md": "部署设计.md",
45
+ "安全设计.md": "安全设计.md",
46
+ }
47
+
48
+ # 模板中的"模板使用说明(必读)"与"附录:生成流程"属于模板自身的元说明,
49
+ # 产物中不需要照抄,仅作为写作纪律存在 —— 列入豁免名单。
50
+ EXEMPT_HEADING_PATTERNS = [
51
+ r"^模版使用说明",
52
+ r"^模板使用说明",
53
+ r"^附录\s*A:生成流程",
54
+ r"^附录\s*B:解析\s*Skill",
55
+ r"^附录\s*B:配套工具清单",
56
+ r"^附录:配套工具",
57
+ r"^附录:生成流程",
58
+ ]
59
+
60
+ # 残留占位符:出现即判 FAIL(说明章节没真写)
61
+ # 注意:只匹配"真实残留的占位标记",不匹配"讨论占位符这件事的说明文字"
62
+ PLACEHOLDER_PATTERNS = [
63
+ r"<<[^>\n]*>>", # <<占位>>
64
+ r"<请[^>\n]*>", # <请填写>
65
+ r"<待[^>\n]*>", # <待补充>
66
+ r"<[Yy][Yy][Yy][Yy]-[Mm][Mm]-[Dd][Dd]>", # <YYYY-MM-DD>
67
+ r"(?m)^\s*(TODO|TBD|FIXME)\s*[::]",
68
+ r"^\s*[-*|]?\s*(TODO|TBD)\s*$",
69
+ ]
70
+
71
+ # 低信息量填充词:单独成段出现即判 WARN
72
+ THIN_FILLER = {"无", "待定", "同左", "同上", "略", "-", "—", "n/a", "N/A"}
73
+
74
+ HEADING_RE = re.compile(r"^(#{1,4})\s+(.+?)\s*$")
75
+
76
+
77
+ class Finding:
78
+ def __init__(self, level: str, code: str, message: str):
79
+ self.level = level # FAIL | WARN | INFO
80
+ self.code = code
81
+ self.message = message
82
+
83
+ def as_dict(self) -> Dict[str, str]:
84
+ return {"level": self.level, "code": self.code, "message": self.message}
85
+
86
+
87
+ def resolve_template_dir(explicit: str | None) -> str:
88
+ candidates: List[str] = []
89
+ if explicit:
90
+ candidates.append(explicit)
91
+ env = os.environ.get(TEMPLATE_DIR_ENV)
92
+ if env:
93
+ candidates.append(env)
94
+ candidates.extend(DEFAULT_TEMPLATE_DIRS)
95
+ for c in candidates:
96
+ if c and os.path.isdir(c):
97
+ return os.path.abspath(c)
98
+ sys.stderr.write(
99
+ "[FATAL] 未找到模板目录。请通过 --template-dir 或环境变量 "
100
+ f"{TEMPLATE_DIR_ENV} 指定。\n"
101
+ )
102
+ sys.exit(2)
103
+
104
+
105
+ def read_lines(path: str) -> List[str]:
106
+ with open(path, "r", encoding="utf-8") as f:
107
+ return f.read().splitlines()
108
+
109
+
110
+ def extract_headings(lines: List[str]) -> List[Tuple[int, str]]:
111
+ """返回 [(level, text)],跳过代码块(``` 围栏)内的伪标题。"""
112
+ out: List[Tuple[int, str]] = []
113
+ in_fence = False
114
+ for line in lines:
115
+ stripped = line.strip()
116
+ if stripped.startswith("```"):
117
+ in_fence = not in_fence
118
+ continue
119
+ if in_fence:
120
+ continue
121
+ m = HEADING_RE.match(line)
122
+ if m:
123
+ out.append((len(m.group(1)), m.group(2).strip()))
124
+ return out
125
+
126
+
127
+ def is_exempt(text: str) -> bool:
128
+ return any(re.match(p, text) for p in EXEMPT_HEADING_PATTERNS)
129
+
130
+
131
+ def normalize(text: str) -> str:
132
+ """归一化标题文本:去掉反引号、多余空白、全角/半角差异。"""
133
+ t = text.replace("`", "")
134
+ t = re.sub(r"\s+", "", t)
135
+ t = t.replace("(", "(").replace(")", ")")
136
+ t = t.replace(":", ":").replace(",", ",")
137
+ return t
138
+
139
+
140
+ def is_pattern_heading(text: str) -> bool:
141
+ """模板里的参数化/可重复标题,产物只需出现同层同级同类即可。"""
142
+ return ("<" in text and ">" in text) or "~" in text or "结构同" in text
143
+
144
+
145
+ def pattern_prefix(text: str) -> str:
146
+ """取参数化标题的固定前缀(第一个 < 或 ~ 之前的部分)。"""
147
+ idx = len(text)
148
+ for ch in ("<", "~"):
149
+ p = text.find(ch)
150
+ if p != -1:
151
+ idx = min(idx, p)
152
+ return normalize(text[:idx])
153
+
154
+
155
+ def section_body(lines: List[str], start_idx: int, end_idx: int) -> str:
156
+ body = "\n".join(lines[start_idx:end_idx])
157
+ # 去掉围栏代码块后统计有效字符
158
+ body = re.sub(r"```.*?```", "", body, flags=re.S)
159
+ return body.strip()
160
+
161
+
162
+ def validate_one(doc_path: str, template_dir: str) -> Tuple[List[Finding], Dict]:
163
+ findings: List[Finding] = []
164
+ doc_name = os.path.basename(doc_path)
165
+ tpl_name = DOC_TEMPLATE_MAP.get(doc_name, doc_name)
166
+ tpl_path = os.path.join(template_dir, tpl_name)
167
+
168
+ if not os.path.isfile(doc_path):
169
+ return [Finding("FAIL", "E-MISSING-DOC", f"产物文件不存在:{doc_path}")], {
170
+ "doc": doc_name
171
+ }
172
+ if not os.path.isfile(tpl_path):
173
+ return [Finding("FAIL", "E-MISSING-TPL", f"模板文件不存在:{tpl_path}")], {
174
+ "doc": doc_name
175
+ }
176
+
177
+ doc_lines = read_lines(doc_path)
178
+ tpl_lines = read_lines(tpl_path)
179
+ doc_h = extract_headings(doc_lines)
180
+ tpl_h = extract_headings(tpl_lines)
181
+
182
+ # 1) 必填章节覆盖检查
183
+ required = [(lv, tx) for lv, tx in tpl_h if not is_exempt(tx)]
184
+ doc_norm = [(lv, normalize(tx)) for lv, tx in doc_h]
185
+
186
+ # 声明式裁剪:解析 §0 修订记录中的「未启用:§x.x(理由:...)」声明
187
+ # 依据:模板《模版使用约定(按需裁剪)》允许整节略过,但要求显式声明。
188
+ # 主理人补充裁定:允许略过,但**禁止序号顺延**,以保持跨文档引用与交叉 diff 稳定。
189
+ declared_omissions: set = set()
190
+ for line in doc_lines:
191
+ for m in re.finditer(r"未启用\s*[::]\s*§?\s*(\d+(?:\.\d+)*)", line):
192
+ declared_omissions.add(m.group(1))
193
+
194
+ def sec_number(text: str) -> str:
195
+ m = re.match(r"^(\d+(?:\.\d+)*)", text.strip())
196
+ return m.group(1) if m else ""
197
+
198
+ missing: List[str] = []
199
+ declared: List[str] = []
200
+ matched_doc_idx = set()
201
+ for lv, tx in required:
202
+ if is_pattern_heading(tx):
203
+ prefix = pattern_prefix(tx)
204
+ hit = False
205
+ for i, (dlv, dnorm) in enumerate(doc_norm):
206
+ if dlv == lv and prefix and dnorm.startswith(prefix) and i not in matched_doc_idx:
207
+ hit = True
208
+ matched_doc_idx.add(i)
209
+ break
210
+ if not hit:
211
+ missing.append(f"L{lv} {tx} (参数化章节:需至少 1 个同类章节)")
212
+ continue
213
+ target = normalize(tx)
214
+ hit = False
215
+ for i, (dlv, dnorm) in enumerate(doc_norm):
216
+ if dlv == lv and dnorm == target and i not in matched_doc_idx:
217
+ hit = True
218
+ matched_doc_idx.add(i)
219
+ break
220
+ if not hit:
221
+ missing.append(f"L{lv} {tx}")
222
+
223
+ for m in missing:
224
+ # 从 "L3 4.4 缓存与 Key 规范" 中取出 "4.4"
225
+ title = re.sub(r"^L\d+\s+", "", m)
226
+ num = sec_number(title)
227
+ # 层级继承:声明 §4.4 即覆盖 §4.4 及其全部子节(§4.4.1 / §4.4.2 / ...)
228
+ covered = num and any(
229
+ num == d or num.startswith(d + ".") for d in declared_omissions
230
+ )
231
+ if covered:
232
+ declared.append(title)
233
+ findings.append(
234
+ Finding(
235
+ "INFO",
236
+ "I-DECLARED-OMISSION",
237
+ f"按模板《按需裁剪》约定豁免(已在 §0 修订记录声明未启用):{title}",
238
+ )
239
+ )
240
+ else:
241
+ findings.append(
242
+ Finding(
243
+ "FAIL",
244
+ "E-MISSING-SECTION",
245
+ f"缺失模板章节:{m}"
246
+ + (
247
+ ""
248
+ if not num
249
+ else f" → 若确实不适用,须在 §0 修订记录声明「未启用:§{num}(理由:...)」方可豁免"
250
+ ),
251
+ )
252
+ )
253
+
254
+ # 2.5) 序号顺延检测(主理人裁定禁止顺延,避免跨文档引用错位)
255
+ tpl_num_titles: Dict[str, str] = {}
256
+ for lv, tx in required:
257
+ n = sec_number(tx)
258
+ if n:
259
+ tpl_num_titles.setdefault(n, normalize(tx))
260
+ for lv, tx in doc_h:
261
+ n = sec_number(tx)
262
+ if not n or n not in tpl_num_titles:
263
+ continue
264
+ if normalize(tx) != tpl_num_titles[n] and not tpl_num_titles[n].startswith(normalize(tx)[:0] or "\0"):
265
+ findings.append(
266
+ Finding(
267
+ "WARN",
268
+ "W-NUMBER-REUSE",
269
+ f"疑似序号顺延:文档 §{n} 标题为「{tx}」,模板 §{n} 为「{tpl_num_titles[n]}」"
270
+ f" → 裁定:保留原编号、允许不连续,禁止顺延",
271
+ )
272
+ )
273
+
274
+ # 2) 章节顺序检查(以模板中出现的相对顺序为基线)
275
+ tpl_seq = [
276
+ (normalize(tx) if not is_pattern_heading(tx) else pattern_prefix(tx), lv, tx)
277
+ for lv, tx in required
278
+ ]
279
+ doc_norms = [(i, lv, normalize(tx)) for i, (lv, tx) in enumerate(doc_h)]
280
+
281
+ cursor = -1
282
+ for key, lv, tx in tpl_seq:
283
+ if not key:
284
+ continue
285
+ # 参数化标题用前缀匹配,普通标题用精确匹配;只接受层次一致的命中
286
+ is_param = is_pattern_heading(tx)
287
+ cands = [
288
+ i
289
+ for i, dlv, dnorm in doc_norms
290
+ if i > cursor
291
+ and dlv == lv
292
+ and (dnorm.startswith(key) if is_param else dnorm == key)
293
+ ]
294
+ if not cands:
295
+ findings.append(
296
+ Finding("WARN", "W-SECTION-ORDER", f"章节顺序与模板不一致或缺失:{tx}")
297
+ )
298
+ else:
299
+ cursor = cands[0]
300
+
301
+ # 3) 空章节检查(跳过模板元说明区与代码块)
302
+ heading_line_idx: List[int] = []
303
+ in_fence = False
304
+ for i, line in enumerate(doc_lines):
305
+ if line.strip().startswith("```"):
306
+ in_fence = not in_fence
307
+ continue
308
+ if not in_fence and HEADING_RE.match(line):
309
+ heading_line_idx.append(i)
310
+ heading_line_idx.append(len(doc_lines))
311
+
312
+ # 元说明区(模版使用说明 / 附录·生成流程 等)内的行不做内容类检查
313
+ exempt_ranges: List[Tuple[int, int]] = []
314
+ for k in range(len(heading_line_idx) - 1):
315
+ s = heading_line_idx[k]
316
+ e = heading_line_idx[k + 1]
317
+ title = doc_lines[s].lstrip("#").strip()
318
+ if is_exempt(title):
319
+ exempt_ranges.append((s, e))
320
+
321
+ def in_exempt(lineno: int) -> bool:
322
+ return any(a <= lineno < b for a, b in exempt_ranges)
323
+
324
+ empty_sections: List[str] = []
325
+ for k in range(len(heading_line_idx) - 1):
326
+ s = heading_line_idx[k]
327
+ e = heading_line_idx[k + 1]
328
+ title = doc_lines[s].lstrip("#").strip()
329
+ if is_exempt(title):
330
+ continue
331
+ body = section_body(doc_lines, s + 1, e)
332
+ if len(body) < 20:
333
+ level = len(doc_lines[s]) - len(doc_lines[s].lstrip("#"))
334
+ empty_sections.append(f"L{level} {title}")
335
+
336
+ for es in empty_sections:
337
+ findings.append(Finding("WARN", "W-EMPTY-SECTION", f"章节内容过短(<20 有效字符):{es}"))
338
+
339
+ # 4) 残留占位符(跳过代码块与元说明区)
340
+ fence = False
341
+ for i, line in enumerate(doc_lines):
342
+ if line.strip().startswith("```"):
343
+ fence = not fence
344
+ continue
345
+ if fence or in_exempt(i):
346
+ continue
347
+ for p in PLACEHOLDER_PATTERNS:
348
+ if re.search(p, line):
349
+ findings.append(
350
+ Finding("FAIL", "E-PLACEHOLDER", f"第 {i + 1} 行残留占位符:{line.strip()[:80]}")
351
+ )
352
+ break
353
+
354
+ # 5) 规模硬指标:产物正文应显著展开,而非仅保留骨架
355
+ total_chars = len("\n".join(doc_lines))
356
+ if total_chars < 3000:
357
+ findings.append(
358
+ Finding("WARN", "W-TOO-SHORT", f"全文仅 {total_chars} 字符,疑似未充分展开(建议 ≥ 8000)")
359
+ )
360
+
361
+ # 6) 特定文档的硬指标章节
362
+ if doc_name in ("material_digest.md", "research_report.md"):
363
+ hard_idx = [i for i, (lv, tx) in enumerate(doc_h) if "硬指标" in tx]
364
+ if not hard_idx:
365
+ findings.append(
366
+ Finding("FAIL", "E-MISSING-HARD-METRICS", f"{doc_name} 必须保留「硬指标清单」章节")
367
+ )
368
+
369
+ summary = {
370
+ "doc": doc_name,
371
+ "template": tpl_name,
372
+ "chars": total_chars,
373
+ "doc_headings": len(doc_h),
374
+ "template_required_headings": len(required),
375
+ "missing_sections": len(missing) - len(declared),
376
+ "declared_omissions": len(declared),
377
+ "empty_sections": len(empty_sections),
378
+ "fail": sum(1 for f in findings if f.level == "FAIL"),
379
+ "warn": sum(1 for f in findings if f.level == "WARN"),
380
+ }
381
+ return findings, summary
382
+
383
+
384
+ def main() -> int:
385
+ ap = argparse.ArgumentParser(description="AICoding 架构文档模板合规校验器")
386
+ ap.add_argument("--output-dir", default=".workbuddy/output", help="产物目录")
387
+ ap.add_argument("--filter", default=None, help="只校验指定文件名(可逗号分隔)")
388
+ ap.add_argument("--template-dir", default=None, help="模板目录")
389
+ ap.add_argument("--json", action="store_true", help="输出 JSON")
390
+ args = ap.parse_args()
391
+
392
+ template_dir = resolve_template_dir(args.template_dir)
393
+ output_dir = os.path.abspath(args.output_dir)
394
+
395
+ if not os.path.isdir(output_dir):
396
+ sys.stderr.write(f"[FATAL] 产物目录不存在:{output_dir}\n")
397
+ return 2
398
+
399
+ targets = [d for d in DOC_TEMPLATE_MAP if os.path.isfile(os.path.join(output_dir, d))]
400
+ if args.filter:
401
+ wanted = [x.strip() for x in args.filter.split(",") if x.strip()]
402
+ targets = [d for d in (wanted if wanted else []) ]
403
+ for w in wanted:
404
+ if not os.path.isfile(os.path.join(output_dir, w)):
405
+ sys.stderr.write(f"[FATAL] --filter 指定的文件不存在:{w}\n")
406
+ return 2
407
+ if not targets:
408
+ sys.stderr.write(f"[FATAL] {output_dir} 下没有可校验的产物文档\n")
409
+ return 2
410
+
411
+ all_findings: Dict[str, List[Finding]] = {}
412
+ summaries: List[Dict] = []
413
+ for name in targets:
414
+ findings, summary = validate_one(os.path.join(output_dir, name), template_dir)
415
+ all_findings[name] = findings
416
+ summaries.append(summary)
417
+
418
+ total_fail = sum(s["fail"] for s in summaries)
419
+ total_warn = sum(s["warn"] for s in summaries)
420
+
421
+ if args.json:
422
+ print(
423
+ json.dumps(
424
+ {
425
+ "output_dir": output_dir,
426
+ "template_dir": template_dir,
427
+ "total_fail": total_fail,
428
+ "total_warn": total_warn,
429
+ "summaries": summaries,
430
+ "findings": {k: [f.as_dict() for f in v] for k, v in all_findings.items()},
431
+ },
432
+ ensure_ascii=False,
433
+ indent=2,
434
+ )
435
+ )
436
+ return 0 if total_fail == 0 else 1
437
+
438
+ print("=" * 78)
439
+ print("AICoding 架构文档 · 模板合规校验报告")
440
+ print("=" * 78)
441
+ print(f"产物目录 : {output_dir}")
442
+ print(f"模板目录 : {template_dir}")
443
+ print(f"校验文档 : {len(targets)} 份")
444
+ print("-" * 78)
445
+ for s in summaries:
446
+ verdict = "PASS" if s["fail"] == 0 else "FAIL"
447
+ print(
448
+ f"[{verdict}] {s['doc']:<20} 字符={s['chars']:<6} "
449
+ f"章节 {s['doc_headings']}/{s['template_required_headings']} "
450
+ f"缺章={s['missing_sections']} 声明豁免={s['declared_omissions']} "
451
+ f"空章={s['empty_sections']} "
452
+ f"FAIL={s['fail']} WARN={s['warn']}"
453
+ )
454
+ print("-" * 78)
455
+ for name in targets:
456
+ fs = all_findings[name]
457
+ if not fs:
458
+ continue
459
+ print(f"\n■ {name}")
460
+ for f in fs:
461
+ print(f" [{f.level:<4}] {f.code}: {f.message}")
462
+ print("-" * 78)
463
+ print(f"合计:FAIL={total_fail} WARN={total_warn}")
464
+ print("结论:" + ("自动校验通过,可进入人工审核。" if total_fail == 0 else "自动校验未通过,禁止进入人工审核。"))
465
+ print("=" * 78)
466
+ return 0 if total_fail == 0 else 1
467
+
468
+
469
+ if __name__ == "__main__":
470
+ sys.exit(main())
@@ -0,0 +1,5 @@
1
+ # dsh bundle patch — 无此文件则 dsh plugin add 不会挂层
2
+ # IDE 若按 RFC6902 报缺字段,属误报;本文件必须保持 - insert(勿改成 JSON Patch)
3
+ - insert:
4
+ - id: coding-kit
5
+ name: dsh-coding-kit
@@ -0,0 +1,68 @@
1
+ export declare const PRE30_HATS: string[];
2
+ export declare function extractHatsFromInvokeName(name: string): string[];
3
+ export declare function resolveRequiredInvokeHats(meta: Record<string, string>): {
4
+ required: string[];
5
+ source: string;
6
+ };
7
+ export declare function collectInvokeHats(target: string, slug: string): Set<string>;
8
+ export declare function missingInvokeHats(target: string, slug: string, hats: string[]): string[];
9
+ export declare function checkPre30InvokeHats(target: string, absTask: string): {
10
+ ok: boolean;
11
+ missing: string[];
12
+ preRequired: string[];
13
+ source: string;
14
+ };
15
+ export type CloseGuardOutcome = {
16
+ status: 'pass' | 'fail' | 'warn';
17
+ detail: string;
18
+ };
19
+ export declare const UNCHECKED_RE: RegExp;
20
+ export declare const CLOSE_STATUSES: Set<string>;
21
+ export declare function taskTargetRoot(absTask: string): string;
22
+ export declare function evalCloseSlug(absTask: string, content: string): CloseGuardOutcome;
23
+ export declare function evalCloseSelfCheck(content: string): CloseGuardOutcome;
24
+ export declare function evalCloseAcceptance(content: string): CloseGuardOutcome;
25
+ export declare function evalCloseStatus(content: string): CloseGuardOutcome;
26
+ export declare function evalCloseInvokeHats(absTask: string, content: string): CloseGuardOutcome;
27
+ export declare function evalCloseReview(absTask: string): CloseGuardOutcome;
28
+ export declare function evalCloseGraphDelta(absTask: string, content: string): CloseGuardOutcome;
29
+ export declare function evalCloseKpi(content: string): CloseGuardOutcome;
30
+ export declare function evalCloseExperience(content: string): CloseGuardOutcome;
31
+ export declare function evalCloseWikiDelta(absTask: string, content: string): CloseGuardOutcome;
32
+ export declare function evalCloseWikiPromotion(absTask: string, content: string): CloseGuardOutcome;
33
+ /** 仓级 Hub 闸:`.coding-kit/local.json`(legacy `.cyning-harness/local.json`)的 `close_hub_gate`;缺省 true(默认开)。 */
34
+ export declare function isCloseHubGateEnabled(root: string): boolean;
35
+ export declare function findHubFile(root: string): string | null;
36
+ /** 探测 PR 合入态。测试可设 `DSH_CLOSE_PR_STATE=MERGED|OPEN|NONE` 旁路 gh。 */
37
+ export declare function resolvePrMergedState(root: string, relatedPr?: string): {
38
+ ok: boolean;
39
+ state: string;
40
+ detail: string;
41
+ };
42
+ export declare function evalClosePrMerged(absTask: string, content: string): CloseGuardOutcome;
43
+ export declare function evalCloseHubIndex(absTask: string, _content: string): CloseGuardOutcome;
44
+ /** docs/spec 根级裸 SPEC-*.md(warn · 不 BLOCK)。 */
45
+ export declare function listBareSpecFiles(root: string): string[];
46
+ export declare function evalCloseGuard(guardId: string, absTask: string, content: string): CloseGuardOutcome | null;
47
+ export type LintIssue = {
48
+ rule: string;
49
+ message: string;
50
+ line?: number;
51
+ };
52
+ export declare const PLACEHOLDER_RE: RegExp;
53
+ export declare function extractSpecSlug(specFile: string, content: string): string;
54
+ export declare function shouldSkipSpecAudit(content: string): boolean;
55
+ export declare function findSpecReview(target: string, specFile: string, content: string): boolean;
56
+ export declare function evalSpecReviewsRetention(target: string, absSpec: string, content: string): CloseGuardOutcome;
57
+ export declare function findReview(target: string, taskFile: string): boolean;
58
+ export declare function runTestCheck(target: string, taskFile: string | undefined): {
59
+ ok: boolean;
60
+ reason: string;
61
+ };
62
+ export declare function lintTaskFile(filePath: string, cwd: string): {
63
+ ok: boolean;
64
+ errors: LintIssue[];
65
+ warnings: LintIssue[];
66
+ file: string;
67
+ slug: string;
68
+ };