@surlymochan/evalid 0.0.0-stage → 0.3.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 (192) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/LICENSE +21 -0
  3. package/README.md +315 -2
  4. package/RELEASE_NOTES.md +102 -0
  5. package/atoms/registry.json +758 -0
  6. package/bin/xflow.js +7208 -0
  7. package/docs/adoption/README.md +128 -0
  8. package/docs/adoption/openflow-release-hardening.md +61 -0
  9. package/docs/benchmark-openflow-vs-gstack-superpowers-openspec-2026-05-25.md +81 -0
  10. package/docs/compatibility.md +52 -0
  11. package/docs/competitive-benchmark.md +108 -0
  12. package/docs/corps-operator-guide.md +75 -0
  13. package/docs/demo-proof.md +126 -0
  14. package/docs/examples-gallery.md +115 -0
  15. package/docs/fixtures/tracker-item.json +7 -0
  16. package/docs/goal-vs-codex.md +129 -0
  17. package/docs/gstack-comparison.md +88 -0
  18. package/docs/install-upgrade.md +133 -0
  19. package/docs/integrations.md +49 -0
  20. package/docs/launch-demo.md +123 -0
  21. package/docs/launch-dossier.md +180 -0
  22. package/docs/methodology.md +149 -0
  23. package/docs/npm-publish-handoff.md +83 -0
  24. package/docs/openspec-migration.md +106 -0
  25. package/docs/public-benchmark.md +68 -0
  26. package/docs/public-release.md +145 -0
  27. package/docs/quality-assessment.md +139 -0
  28. package/docs/quickstart.md +156 -0
  29. package/docs/reviewer-guide.md +43 -0
  30. package/docs/spec-kit-benchmark.md +73 -0
  31. package/docs/super-assistant-comparison.md +106 -0
  32. package/docs/superpowers-comparison.md +82 -0
  33. package/docs/team-adoption.md +123 -0
  34. package/docs/tooling-matrix.md +91 -0
  35. package/docs/walkthrough.md +76 -0
  36. package/docs/workflow-manual.md +1125 -0
  37. package/package.json +83 -4
  38. package/prompts/pencil-draft.md +46 -0
  39. package/prompts/pencil-refine.md +46 -0
  40. package/prompts/phase-artifact.md +24 -0
  41. package/prompts/phase-execution.md +32 -0
  42. package/schemas/competitor-reconstruction-contract.schema.json +91 -0
  43. package/schemas/llm-design-review-aggregate.schema.json +83 -0
  44. package/schemas/visual-benchmark.schema.json +978 -0
  45. package/schemas/workflow.schema.json +100 -0
  46. package/scripts/run-tests.mjs +23 -0
  47. package/scripts/takein_digest.py +139 -0
  48. package/specs/README.md +118 -0
  49. package/specs/architecture.md +36 -0
  50. package/specs/product.md +33 -0
  51. package/specs/workflow.md +349 -0
  52. package/src/agent_team/atoms/codex-app-bridge.js +8 -0
  53. package/src/agent_team/atoms/design/competitor-reconstruction-review.js +44 -0
  54. package/src/agent_team/atoms/design/contract-freeze.js +8 -0
  55. package/src/agent_team/atoms/design/generation-contract.js +442 -0
  56. package/src/agent_team/atoms/design/reconstruction-pack.js +246 -0
  57. package/src/agent_team/atoms/design/reference-surface-lock.js +49 -0
  58. package/src/agent_team/atoms/patch-challenge.js +19 -0
  59. package/src/agent_team/atoms/pencil/accept.js +9 -0
  60. package/src/agent_team/atoms/pencil/draft.js +365 -0
  61. package/src/agent_team/atoms/pencil/refine.js +229 -0
  62. package/src/agent_team/atoms/phase-run-dispatch.js +8 -0
  63. package/src/agent_team/atoms/team-run.js +380 -0
  64. package/src/agent_team/atoms/visual/aesthetic-review.js +231 -0
  65. package/src/agent_team/atoms/visual/benchmark-repair-plan.js +291 -0
  66. package/src/agent_team/atoms/visual/benchmark.js +1652 -0
  67. package/src/agent_team/atoms/visual/design-selection.js +23 -0
  68. package/src/agent_team/atoms/visual/direction-synthesis.js +124 -0
  69. package/src/agent_team/atoms/visual/dom-rect-export.js +129 -0
  70. package/src/agent_team/atoms/visual/image-diff.js +186 -0
  71. package/src/agent_team/atoms/visual/layout-competition.js +8 -0
  72. package/src/agent_team/atoms/visual/page-evidence-capture.js +806 -0
  73. package/src/agent_team/atoms/visual/playwright-runtime.js +8 -0
  74. package/src/agent_team/atoms/visual/review-aggregate.js +87 -0
  75. package/src/agent_team/atoms/visual/review.js +77 -0
  76. package/src/agent_team/atoms/visual/visual-report.js +197 -0
  77. package/src/agent_team/atoms/visual/visual-token-export.js +417 -0
  78. package/src/agent_team/common.js +311 -0
  79. package/src/core/atoms/agent-invoke.js +684 -0
  80. package/src/core/atoms/event/ingest.js +30 -0
  81. package/src/core/atoms/experience/digest.js +55 -0
  82. package/src/core/atoms/gate/artifact-verify.js +83 -0
  83. package/src/core/atoms/gate/competitor-contract-validate.js +119 -0
  84. package/src/core/atoms/gate/evidence-collect.js +64 -0
  85. package/src/core/atoms/gate/payload-from-phase-run.js +78 -0
  86. package/src/core/atoms/gate/visual-benchmark-validate.js +183 -0
  87. package/src/core/atoms/gate/workbench.js +79 -0
  88. package/src/core/atoms/intake/classify.js +15 -0
  89. package/src/core/atoms/intake/design-skill-select.js +16 -0
  90. package/src/core/atoms/intake/preview-or-refine.js +19 -0
  91. package/src/core/atoms/intake/recommend.js +20 -0
  92. package/src/core/atoms/intake/research-same-category.js +19 -0
  93. package/src/core/atoms/intervention/create.js +30 -0
  94. package/src/core/atoms/memory/manage.js +27 -0
  95. package/src/core/atoms/memory/mount.js +13 -0
  96. package/src/core/atoms/mission/create.js +19 -0
  97. package/src/core/atoms/mission/list.js +14 -0
  98. package/src/core/atoms/mission/show.js +26 -0
  99. package/src/core/atoms/notify/checkpoint.js +46 -0
  100. package/src/core/atoms/phase-run/complete.js +42 -0
  101. package/src/core/atoms/phase-run/fail.js +40 -0
  102. package/src/core/atoms/phase-run/persist.js +39 -0
  103. package/src/core/atoms/phase-run/start.js +30 -0
  104. package/src/core/atoms/plugin/run.js +76 -0
  105. package/src/core/atoms/policy/gate.js +90 -0
  106. package/src/core/atoms/policy/resolve.js +11 -0
  107. package/src/core/atoms/storage/sync.js +57 -0
  108. package/src/core/atoms/workflow/advance.js +25 -0
  109. package/src/core/atoms/workflow/guard.js +127 -0
  110. package/src/core/atoms/workflow/state-load.js +37 -0
  111. package/src/core/bindings.js +37 -0
  112. package/src/core/change-artifacts.js +134 -0
  113. package/src/core/competitive-score.js +265 -0
  114. package/src/core/concurrency-policy.js +35 -0
  115. package/src/core/execution-log.js +108 -0
  116. package/src/core/intake-recommendation.js +134 -0
  117. package/src/core/memory-state.js +62 -0
  118. package/src/core/mission-state.js +780 -0
  119. package/src/core/pending-gates.js +66 -0
  120. package/src/core/phase-cost.js +116 -0
  121. package/src/core/phase-run-read.js +27 -0
  122. package/src/core/policy-overlay.js +180 -0
  123. package/src/core/state-store.js +201 -0
  124. package/src/core/workflow-executor.js +998 -0
  125. package/src/core/workflow-integrity.js +111 -0
  126. package/src/core/workflow-loader.js +163 -0
  127. package/src/core/workflow-rerun.js +84 -0
  128. package/src/core/workflow-state.js +119 -0
  129. package/src/server.js +1307 -0
  130. package/workflows/corps.yaml +479 -0
  131. package/workflows/yolo.yaml +233 -0
  132. package/xflow/README.md +76 -0
  133. package/xflow/aha/SKILL.md +82 -0
  134. package/xflow/aha/scripts/append_aha.py +50 -0
  135. package/xflow/archive/scripts/archive_change.py +338 -0
  136. package/xflow/archive/templates/merge-design.md +54 -0
  137. package/xflow/archive/templates/merge-workflow.md +26 -0
  138. package/xflow/atoms/__init__.py +6 -0
  139. package/xflow/atoms/a1_issue_create_or_patch.py +70 -0
  140. package/xflow/atoms/a2_branch_create_and_link.py +42 -0
  141. package/xflow/atoms/a3_repo_doctor.py +11 -0
  142. package/xflow/atoms/a4_project_set_status.py +11 -0
  143. package/xflow/atoms/a5_archive_commit_push_close.py +11 -0
  144. package/xflow/atoms/a6_pr_create.py +72 -0
  145. package/xflow/atoms/b1_change_scaffold.py +11 -0
  146. package/xflow/atoms/b2_status_read.py +16 -0
  147. package/xflow/atoms/b2b_status_write.py +20 -0
  148. package/xflow/atoms/b3_status_transition.py +27 -0
  149. package/xflow/atoms/b4_handoff_scaffold_or_refresh.py +11 -0
  150. package/xflow/atoms/b5_aha_append.py +11 -0
  151. package/xflow/atoms/b6_takein_digest.py +11 -0
  152. package/xflow/atoms/b6b_lookback_digest.py +11 -0
  153. package/xflow/atoms/b7_change_mission_bind.py +43 -0
  154. package/xflow/atoms/c1_mem.py +54 -0
  155. package/xflow/atoms/e6_gate_local_precheck.py +23 -0
  156. package/xflow/atoms/h1_design_lite_gate.py +74 -0
  157. package/xflow/atoms/i6a_tdd_run.py +104 -0
  158. package/xflow/atoms/i6b_tdd_proof_validate.py +165 -0
  159. package/xflow/atoms/i6c_tdd_quality_review.py +357 -0
  160. package/xflow/atoms/j1_tests_run.py +129 -0
  161. package/xflow/atoms/j4a_spec_consistency_structural.py +105 -0
  162. package/xflow/atoms/j4b_spec_delta_review.py +316 -0
  163. package/xflow/atoms/j4c_openspec_migration_map.py +121 -0
  164. package/xflow/atoms/j4d_spec_clarify_gate.py +373 -0
  165. package/xflow/atoms/j6_cr_review.py +443 -0
  166. package/xflow/atoms/k1_artifacts_complete_check.py +39 -0
  167. package/xflow/atoms/k2_merge_snippets_apply.py +11 -0
  168. package/xflow/atoms/k3_handoff_refresh.py +14 -0
  169. package/xflow/atoms/k4_mem_lesson_persist.py +65 -0
  170. package/xflow/atoms/k5_archive_publish.py +21 -0
  171. package/xflow/atoms/k6_aha_merge.py +42 -0
  172. package/xflow/atoms/tdd_lib.py +747 -0
  173. package/xflow/corps/SKILL.md +109 -0
  174. package/xflow/exec/scripts/set_issue_in_progress.py +159 -0
  175. package/xflow/goal/SKILL.md +150 -0
  176. package/xflow/handoff/SKILL.md +95 -0
  177. package/xflow/handoff/scripts/scaffold_handoff.py +304 -0
  178. package/xflow/lookback/SKILL.md +66 -0
  179. package/xflow/lookback/scripts/lookback_digest.py +46 -0
  180. package/xflow/openissue/scripts/open_issue_flow.py +438 -0
  181. package/xflow/plan/SKILL.md +38 -0
  182. package/xflow/ralph/SKILL.md +123 -0
  183. package/xflow/scripts/check_installed_xflow_skill_sync.sh +44 -0
  184. package/xflow/scripts/common.py +480 -0
  185. package/xflow/scripts/doctor.py +278 -0
  186. package/xflow/scripts/sync_installed_xflow_skill.sh +134 -0
  187. package/xflow/takein/SKILL.md +81 -0
  188. package/xflow/takein/scripts/takein_digest.py +88 -0
  189. package/xflow/yolo/SKILL.md +66 -0
  190. package/xflow/yolo/scripts/scaffold_specs_change.py +673 -0
  191. package/xflow/yolo/scripts/yolo_gate.py +312 -0
  192. package/xflow/yolo/templates/DESIGN.md +179 -0
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ J4d.spec_clarify.gate - Deterministic SPEC clarification gate.
4
+
5
+ Runs BEFORE the implementation loop (proposal-consistency stage). J4a checks
6
+ that spec files are structurally consistent and J4b reviews the delta after
7
+ the fact; neither asks whether the spec is clear enough to implement from.
8
+ This gate scans the change's spec documents for ambiguity that predictably
9
+ turns into extra implementation loops, and for every finding emits a concrete
10
+ clarification question that a human or agent can answer directly:
11
+
12
+ 1. unquantified_normative - a MUST/SHALL (or Chinese normative) statement
13
+ with no measurable anchor (number, unit, literal, comparison, or a
14
+ normative return/be/equal clause as parsed by tdd_lib).
15
+ 2. missing_when_then - a Scenario block without a WHEN trigger or a
16
+ THEN observable outcome.
17
+ 3. vague_term - a vague quality word (EN + CN) that the same
18
+ line does not quantify.
19
+ 4. unresolved_placeholder - TBD/TODO/<placeholder>/Chinese pending markers
20
+ left inside a requirement.
21
+
22
+ False-positive control is the design constraint: fenced code is ignored,
23
+ prohibitions (MUST NOT ...) are exempt from (1) because absence is testable,
24
+ a vague word quantified on the same line is not reported, a line already
25
+ flagged by (1) is not double-flagged by (3), and pronoun/acronym guessing is
26
+ deliberately NOT implemented. Output is structured JSON plus a markdown
27
+ checklist (spec_clarify.json / spec_clarify.md in the change dir).
28
+ """
29
+ import argparse
30
+ import json
31
+ import re
32
+ import sys
33
+ from datetime import datetime, timezone
34
+ from pathlib import Path
35
+
36
+ import tdd_lib
37
+
38
+ GENERATED_FILES = {"spec_delta_review.md", "spec_clarify.md"}
39
+
40
+ # --- measurable anchors -----------------------------------------------------
41
+ DIGIT = re.compile(r"\d")
42
+ LITERAL = re.compile(r"`[^`]+`|\"[^\"]+\"|'[^']+'")
43
+ COMPARISON = re.compile(
44
+ r"(>=|<=|==|>|<|\bat least\b|\bat most\b|\bno more than\b|\bwithin\b|"
45
+ r"不超过|不低于|至少|至多|以内|以上|以下)",
46
+ re.IGNORECASE,
47
+ )
48
+
49
+ # --- normative markers ------------------------------------------------------
50
+ NORMATIVE_EN = re.compile(r"\b(MUST|SHALL)\b")
51
+ NORMATIVE_CN = re.compile(r"(必须|应当|须确保|须)")
52
+ PROHIBITION = re.compile(r"\b(MUST|SHALL)\s+NOT\b|禁止|不得", re.IGNORECASE)
53
+
54
+ # Quality / abstract words that make a normative statement untestable when
55
+ # nothing on the line pins them down.
56
+ QUALITY_WORDS = [
57
+ "fast", "quickly", "slow", "responsive", "efficient", "scalable",
58
+ "reliable", "robust", "stable", "secure", "flexible", "intuitive",
59
+ "user-friendly", "seamless", "performant", "high-performance",
60
+ "low-latency", "real-time",
61
+ "快速", "高效", "低延迟", "高性能", "稳定", "可靠", "灵活", "实时", "友好",
62
+ ]
63
+ ABSTRACT_VERBS = re.compile(
64
+ r"\b(handle|support|ensure|provide|manage|process)\b", re.IGNORECASE
65
+ )
66
+ ABSTRACT_NOUNS = [
67
+ "errors", "performance", "security", "scalability", "reliability",
68
+ "load", "traffic",
69
+ ]
70
+
71
+ # --- vague terms (category 3) ----------------------------------------------
72
+ VAGUE_TERMS = {
73
+ # performance dimension -> threshold question
74
+ "fast": "perf", "quickly": "perf", "slow": "perf", "slowly": "perf",
75
+ "responsive": "perf", "efficient": "perf", "efficiently": "perf",
76
+ "low-latency": "perf", "high-performance": "perf",
77
+ "快速": "perf", "高效": "perf", "低延迟": "perf", "高性能": "perf",
78
+ "及时": "perf", "尽快": "perf",
79
+ # quality dimension -> criterion question
80
+ "properly": "criterion", "appropriate": "criterion",
81
+ "appropriately": "criterion", "reasonable": "criterion",
82
+ "reasonably": "criterion", "user-friendly": "criterion",
83
+ "intuitive": "criterion", "robust": "criterion", "seamless": "criterion",
84
+ "adequate": "criterion", "sufficient": "criterion", "flexible": "criterion",
85
+ "合理": "criterion", "适当": "criterion", "妥善": "criterion",
86
+ "友好": "criterion", "易用": "criterion", "良好": "criterion",
87
+ "足够": "criterion", "灵活": "criterion", "稳定": "criterion",
88
+ "可靠": "criterion",
89
+ # scale dimension -> capacity question
90
+ "scalable": "scale", "large-scale": "scale",
91
+ "可扩展": "scale",
92
+ }
93
+
94
+ PLACEHOLDERS = [
95
+ (re.compile(r"\bTBD\b|\bTODO\b|\bXXX\b|\?\?\?"), "placeholder"),
96
+ (re.compile(r"<[a-z][a-z0-9_-]*>"), "placeholder"),
97
+ (re.compile(r"待定|待补充|待确认|未定|后续再定"), "placeholder"),
98
+ ]
99
+
100
+ SCENARIO_START = re.compile(
101
+ r"^\s*(?:#{1,4}\s*)?(?:Scenario|场景)(?=[\s::]|$)\s*[::]?\s*(.*)$",
102
+ re.IGNORECASE,
103
+ )
104
+ HEADING = re.compile(r"^\s*#{1,4}\s+")
105
+ WHEN = re.compile(r"\bWHEN\b|当[^。;\n]{0,60}时")
106
+ THEN = re.compile(r"\bTHEN\b|则|期望结果|预期结果")
107
+
108
+
109
+ CONCRETE_CLAUSE_VALUES = {"true", "false", "null", "none", "nil"}
110
+
111
+
112
+ def has_anchor(line: str, clauses: list[dict], line_number: int) -> bool:
113
+ if DIGIT.search(line) or LITERAL.search(line) or COMPARISON.search(line):
114
+ return True
115
+ # A normative clause (MUST return/be/equal <value>) is only an anchor when
116
+ # its value is concrete. "MUST be fast" parses as a clause with value
117
+ # "fast" in tdd_lib, but a vague value pins nothing down.
118
+ vague_values = {t.lower() for t in VAGUE_TERMS} | {w.lower() for w in QUALITY_WORDS}
119
+ for clause in clauses:
120
+ if clause["line"] != line_number:
121
+ continue
122
+ value = clause["value"]
123
+ if DIGIT.search(value) or value in CONCRETE_CLAUSE_VALUES or value not in vague_values:
124
+ return True
125
+ return False
126
+
127
+
128
+ def question_for_dimension(dimension: str, term: str, excerpt: str) -> str:
129
+ if dimension == "perf":
130
+ return (
131
+ f"「{term}」的验收阈值是多少?请给数值+单位(如 p95 ≤ N ms)、"
132
+ f"测量方法与负载条件(并发/数据量),原文:{excerpt}"
133
+ )
134
+ if dimension == "scale":
135
+ return (
136
+ f"「{term}」的目标规模是多少?请给并发量、数据量或增长率的"
137
+ f"具体数值与验证方式,原文:{excerpt}"
138
+ )
139
+ return (
140
+ f"「{term}」的判定标准是什么?请给可测的验收条件"
141
+ f"(数值、枚举或明确的通过/失败判据),原文:{excerpt}"
142
+ )
143
+
144
+
145
+ def strip_code_blocks(text: str) -> list[str]:
146
+ """Return lines with fenced code blocks blanked out (line numbers kept)."""
147
+ out = []
148
+ in_fence = False
149
+ for line in text.splitlines():
150
+ if line.strip().startswith("```"):
151
+ in_fence = not in_fence
152
+ out.append("")
153
+ continue
154
+ out.append("" if in_fence else line)
155
+ return out
156
+
157
+
158
+ def scan_file(path: Path, display: str) -> list[dict]:
159
+ text = path.read_text(encoding="utf-8")
160
+ lines = strip_code_blocks(text)
161
+ clauses = tdd_lib.normative_clauses(text, display)
162
+ findings: list[dict] = []
163
+
164
+ def add(category, line_no, excerpt, question, term=None):
165
+ findings.append({
166
+ "id": f"{display}:{line_no}:{category}",
167
+ "category": category,
168
+ "severity": "blocker",
169
+ "file": display,
170
+ "line": line_no,
171
+ "excerpt": excerpt.strip()[:200],
172
+ "term": term,
173
+ "question": question,
174
+ })
175
+
176
+ normative_lines: set[int] = set()
177
+
178
+ # Categories 1, 3, 4 - line level.
179
+ for idx, raw in enumerate(lines, start=1):
180
+ line = raw.strip()
181
+ if not line or HEADING.match(raw):
182
+ continue
183
+ is_normative = bool(NORMATIVE_EN.search(line) or NORMATIVE_CN.search(line))
184
+ anchored = has_anchor(line, clauses, idx)
185
+
186
+ if is_normative and not PROHIBITION.search(line):
187
+ quality_hit = next(
188
+ (w for w in QUALITY_WORDS if w.lower() in line.lower()), None
189
+ )
190
+ abstract_hit = bool(
191
+ ABSTRACT_VERBS.search(line)
192
+ and any(n in line.lower() for n in ABSTRACT_NOUNS)
193
+ )
194
+ if not anchored and (quality_hit or abstract_hit):
195
+ term = quality_hit or "unquantified"
196
+ dimension = VAGUE_TERMS.get(term.lower(), "criterion") if quality_hit else "criterion"
197
+ add(
198
+ "unquantified_normative", idx, line,
199
+ question_for_dimension(dimension, term, line[:80]),
200
+ term=term,
201
+ )
202
+ normative_lines.add(idx)
203
+
204
+ # Vague terms: skip lines already flagged as normative, and lines
205
+ # where the same line quantifies the claim.
206
+ if idx not in normative_lines and not anchored:
207
+ lowered = line.lower()
208
+ for term, dimension in VAGUE_TERMS.items():
209
+ if term in lowered:
210
+ add(
211
+ "vague_term", idx, line,
212
+ question_for_dimension(dimension, term, line[:80]),
213
+ term=term,
214
+ )
215
+ break
216
+
217
+ for pattern, _kind in PLACEHOLDERS:
218
+ match = pattern.search(line)
219
+ if match:
220
+ add(
221
+ "unresolved_placeholder", idx, line,
222
+ f"「{match.group(0)}」尚未定值:它的最终取值/定义是什么、"
223
+ f"由谁在开工前定?原文:{line[:80]}",
224
+ term=match.group(0),
225
+ )
226
+ break
227
+
228
+ # Category 2 - scenario blocks.
229
+ idx = 0
230
+ while idx < len(lines):
231
+ match = SCENARIO_START.match(lines[idx])
232
+ if not match:
233
+ idx += 1
234
+ continue
235
+ title = (match.group(1) or "").strip() or "(untitled)"
236
+ start = idx
237
+ block = [lines[idx]]
238
+ idx += 1
239
+ while idx < len(lines) and not SCENARIO_START.match(lines[idx]) and not HEADING.match(lines[idx]):
240
+ block.append(lines[idx])
241
+ idx += 1
242
+ block_text = "\n".join(block)
243
+ missing = []
244
+ if not WHEN.search(block_text):
245
+ missing.append("WHEN(触发条件)")
246
+ if not THEN.search(block_text):
247
+ missing.append("THEN(可观察的期望结果)")
248
+ if missing:
249
+ add(
250
+ "missing_when_then", start + 1, lines[start],
251
+ f"场景「{title}」缺 {' 和 '.join(missing)}:触发条件是什么?"
252
+ f"触发后可观察的期望结果是什么(状态码/输出/副作用)?",
253
+ term=title,
254
+ )
255
+
256
+ return findings
257
+
258
+
259
+ def collect_change_files(change_dir: Path) -> list[Path]:
260
+ files: list[Path] = []
261
+ for name in ["proposal.md", "plan.md"]:
262
+ candidate = change_dir / name
263
+ if candidate.exists():
264
+ files.append(candidate)
265
+ for candidate in sorted(change_dir.rglob("*.md")):
266
+ if candidate.name in GENERATED_FILES:
267
+ continue
268
+ if candidate not in files:
269
+ files.append(candidate)
270
+ return files
271
+
272
+
273
+ def render_markdown(report: dict) -> str:
274
+ lines = [
275
+ "# Spec Clarification Gate",
276
+ "",
277
+ f"- Change: `{report['change_id']}`",
278
+ f"- Verdict: `{report['verdict']}`",
279
+ f"- Findings: {report['summary']['findings']}",
280
+ "",
281
+ "## Clarification questions (answer in the spec before the implementation loop)",
282
+ "",
283
+ ]
284
+ if report["clarification_questions"]:
285
+ lines.extend(
286
+ f"- [ ] ({f['category']} {f['file']}:{f['line']}) {f['question']}"
287
+ for f in report["findings"]
288
+ )
289
+ else:
290
+ lines.append("- None - the spec is clear enough to implement from.")
291
+ return "\n".join(lines).rstrip() + "\n"
292
+
293
+
294
+ def main() -> None:
295
+ parser = argparse.ArgumentParser()
296
+ parser.add_argument("--project-root", default=".")
297
+ parser.add_argument("--change-id", default="")
298
+ parser.add_argument(
299
+ "--file", action="append", dest="files", default=[],
300
+ help="Scan this spec file directly (repeatable); overrides change-dir scan",
301
+ )
302
+ args = parser.parse_args()
303
+
304
+ project_root = Path(args.project_root).resolve()
305
+ change_dir = project_root / "specs" / "changes" / args.change_id if args.change_id else None
306
+
307
+ targets: list[tuple[Path, str]] = []
308
+ if args.files:
309
+ for raw in args.files:
310
+ path = Path(raw)
311
+ if not path.is_absolute():
312
+ path = project_root / raw
313
+ targets.append((path, raw))
314
+ elif change_dir is not None:
315
+ for path in collect_change_files(change_dir):
316
+ targets.append((path, str(path.relative_to(project_root))))
317
+
318
+ findings: list[dict] = []
319
+ scanned: list[str] = []
320
+ for path, display in targets:
321
+ if not path.exists():
322
+ findings.append({
323
+ "id": f"{display}:0:missing_input",
324
+ "category": "missing_input",
325
+ "severity": "blocker",
326
+ "file": display,
327
+ "line": 0,
328
+ "excerpt": "",
329
+ "term": None,
330
+ "question": f"规格文件 {display} 不存在:先补齐 spec 再过澄清门禁。",
331
+ })
332
+ continue
333
+ scanned.append(display)
334
+ findings.extend(scan_file(path, display))
335
+
336
+ by_category: dict[str, int] = {}
337
+ for finding in findings:
338
+ by_category[finding["category"]] = by_category.get(finding["category"], 0) + 1
339
+
340
+ report = {
341
+ "ok": len(findings) == 0,
342
+ "gate": "J4d.spec_clarify.gate",
343
+ "verdict": "pass" if not findings else "needs_clarification",
344
+ "change_id": args.change_id,
345
+ "files_scanned": scanned,
346
+ "summary": {
347
+ "findings": len(findings),
348
+ "by_category": by_category,
349
+ "questions": len(findings),
350
+ },
351
+ "findings": findings,
352
+ "clarification_questions": [f["question"] for f in findings],
353
+ "advice": (
354
+ "Answer every clarification question in the spec, then re-run this "
355
+ "gate before entering the implementation loop."
356
+ if findings else "Spec is clear enough to implement from."
357
+ ),
358
+ "recorded_at": datetime.now(timezone.utc).isoformat(),
359
+ }
360
+
361
+ if change_dir is not None and not args.files:
362
+ change_dir.mkdir(parents=True, exist_ok=True)
363
+ (change_dir / "spec_clarify.json").write_text(
364
+ json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
365
+ )
366
+ (change_dir / "spec_clarify.md").write_text(render_markdown(report), encoding="utf-8")
367
+
368
+ print(json.dumps(report, indent=2, ensure_ascii=False))
369
+ sys.exit(0 if report["ok"] else 1)
370
+
371
+
372
+ if __name__ == "__main__":
373
+ main()