@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.
- package/CHANGELOG.md +74 -0
- package/LICENSE +21 -0
- package/README.md +315 -2
- package/RELEASE_NOTES.md +102 -0
- package/atoms/registry.json +758 -0
- package/bin/xflow.js +7208 -0
- package/docs/adoption/README.md +128 -0
- package/docs/adoption/openflow-release-hardening.md +61 -0
- package/docs/benchmark-openflow-vs-gstack-superpowers-openspec-2026-05-25.md +81 -0
- package/docs/compatibility.md +52 -0
- package/docs/competitive-benchmark.md +108 -0
- package/docs/corps-operator-guide.md +75 -0
- package/docs/demo-proof.md +126 -0
- package/docs/examples-gallery.md +115 -0
- package/docs/fixtures/tracker-item.json +7 -0
- package/docs/goal-vs-codex.md +129 -0
- package/docs/gstack-comparison.md +88 -0
- package/docs/install-upgrade.md +133 -0
- package/docs/integrations.md +49 -0
- package/docs/launch-demo.md +123 -0
- package/docs/launch-dossier.md +180 -0
- package/docs/methodology.md +149 -0
- package/docs/npm-publish-handoff.md +83 -0
- package/docs/openspec-migration.md +106 -0
- package/docs/public-benchmark.md +68 -0
- package/docs/public-release.md +145 -0
- package/docs/quality-assessment.md +139 -0
- package/docs/quickstart.md +156 -0
- package/docs/reviewer-guide.md +43 -0
- package/docs/spec-kit-benchmark.md +73 -0
- package/docs/super-assistant-comparison.md +106 -0
- package/docs/superpowers-comparison.md +82 -0
- package/docs/team-adoption.md +123 -0
- package/docs/tooling-matrix.md +91 -0
- package/docs/walkthrough.md +76 -0
- package/docs/workflow-manual.md +1125 -0
- package/package.json +83 -4
- package/prompts/pencil-draft.md +46 -0
- package/prompts/pencil-refine.md +46 -0
- package/prompts/phase-artifact.md +24 -0
- package/prompts/phase-execution.md +32 -0
- package/schemas/competitor-reconstruction-contract.schema.json +91 -0
- package/schemas/llm-design-review-aggregate.schema.json +83 -0
- package/schemas/visual-benchmark.schema.json +978 -0
- package/schemas/workflow.schema.json +100 -0
- package/scripts/run-tests.mjs +23 -0
- package/scripts/takein_digest.py +139 -0
- package/specs/README.md +118 -0
- package/specs/architecture.md +36 -0
- package/specs/product.md +33 -0
- package/specs/workflow.md +349 -0
- package/src/agent_team/atoms/codex-app-bridge.js +8 -0
- package/src/agent_team/atoms/design/competitor-reconstruction-review.js +44 -0
- package/src/agent_team/atoms/design/contract-freeze.js +8 -0
- package/src/agent_team/atoms/design/generation-contract.js +442 -0
- package/src/agent_team/atoms/design/reconstruction-pack.js +246 -0
- package/src/agent_team/atoms/design/reference-surface-lock.js +49 -0
- package/src/agent_team/atoms/patch-challenge.js +19 -0
- package/src/agent_team/atoms/pencil/accept.js +9 -0
- package/src/agent_team/atoms/pencil/draft.js +365 -0
- package/src/agent_team/atoms/pencil/refine.js +229 -0
- package/src/agent_team/atoms/phase-run-dispatch.js +8 -0
- package/src/agent_team/atoms/team-run.js +380 -0
- package/src/agent_team/atoms/visual/aesthetic-review.js +231 -0
- package/src/agent_team/atoms/visual/benchmark-repair-plan.js +291 -0
- package/src/agent_team/atoms/visual/benchmark.js +1652 -0
- package/src/agent_team/atoms/visual/design-selection.js +23 -0
- package/src/agent_team/atoms/visual/direction-synthesis.js +124 -0
- package/src/agent_team/atoms/visual/dom-rect-export.js +129 -0
- package/src/agent_team/atoms/visual/image-diff.js +186 -0
- package/src/agent_team/atoms/visual/layout-competition.js +8 -0
- package/src/agent_team/atoms/visual/page-evidence-capture.js +806 -0
- package/src/agent_team/atoms/visual/playwright-runtime.js +8 -0
- package/src/agent_team/atoms/visual/review-aggregate.js +87 -0
- package/src/agent_team/atoms/visual/review.js +77 -0
- package/src/agent_team/atoms/visual/visual-report.js +197 -0
- package/src/agent_team/atoms/visual/visual-token-export.js +417 -0
- package/src/agent_team/common.js +311 -0
- package/src/core/atoms/agent-invoke.js +684 -0
- package/src/core/atoms/event/ingest.js +30 -0
- package/src/core/atoms/experience/digest.js +55 -0
- package/src/core/atoms/gate/artifact-verify.js +83 -0
- package/src/core/atoms/gate/competitor-contract-validate.js +119 -0
- package/src/core/atoms/gate/evidence-collect.js +64 -0
- package/src/core/atoms/gate/payload-from-phase-run.js +78 -0
- package/src/core/atoms/gate/visual-benchmark-validate.js +183 -0
- package/src/core/atoms/gate/workbench.js +79 -0
- package/src/core/atoms/intake/classify.js +15 -0
- package/src/core/atoms/intake/design-skill-select.js +16 -0
- package/src/core/atoms/intake/preview-or-refine.js +19 -0
- package/src/core/atoms/intake/recommend.js +20 -0
- package/src/core/atoms/intake/research-same-category.js +19 -0
- package/src/core/atoms/intervention/create.js +30 -0
- package/src/core/atoms/memory/manage.js +27 -0
- package/src/core/atoms/memory/mount.js +13 -0
- package/src/core/atoms/mission/create.js +19 -0
- package/src/core/atoms/mission/list.js +14 -0
- package/src/core/atoms/mission/show.js +26 -0
- package/src/core/atoms/notify/checkpoint.js +46 -0
- package/src/core/atoms/phase-run/complete.js +42 -0
- package/src/core/atoms/phase-run/fail.js +40 -0
- package/src/core/atoms/phase-run/persist.js +39 -0
- package/src/core/atoms/phase-run/start.js +30 -0
- package/src/core/atoms/plugin/run.js +76 -0
- package/src/core/atoms/policy/gate.js +90 -0
- package/src/core/atoms/policy/resolve.js +11 -0
- package/src/core/atoms/storage/sync.js +57 -0
- package/src/core/atoms/workflow/advance.js +25 -0
- package/src/core/atoms/workflow/guard.js +127 -0
- package/src/core/atoms/workflow/state-load.js +37 -0
- package/src/core/bindings.js +37 -0
- package/src/core/change-artifacts.js +134 -0
- package/src/core/competitive-score.js +265 -0
- package/src/core/concurrency-policy.js +35 -0
- package/src/core/execution-log.js +108 -0
- package/src/core/intake-recommendation.js +134 -0
- package/src/core/memory-state.js +62 -0
- package/src/core/mission-state.js +780 -0
- package/src/core/pending-gates.js +66 -0
- package/src/core/phase-cost.js +116 -0
- package/src/core/phase-run-read.js +27 -0
- package/src/core/policy-overlay.js +180 -0
- package/src/core/state-store.js +201 -0
- package/src/core/workflow-executor.js +998 -0
- package/src/core/workflow-integrity.js +111 -0
- package/src/core/workflow-loader.js +163 -0
- package/src/core/workflow-rerun.js +84 -0
- package/src/core/workflow-state.js +119 -0
- package/src/server.js +1307 -0
- package/workflows/corps.yaml +479 -0
- package/workflows/yolo.yaml +233 -0
- package/xflow/README.md +76 -0
- package/xflow/aha/SKILL.md +82 -0
- package/xflow/aha/scripts/append_aha.py +50 -0
- package/xflow/archive/scripts/archive_change.py +338 -0
- package/xflow/archive/templates/merge-design.md +54 -0
- package/xflow/archive/templates/merge-workflow.md +26 -0
- package/xflow/atoms/__init__.py +6 -0
- package/xflow/atoms/a1_issue_create_or_patch.py +70 -0
- package/xflow/atoms/a2_branch_create_and_link.py +42 -0
- package/xflow/atoms/a3_repo_doctor.py +11 -0
- package/xflow/atoms/a4_project_set_status.py +11 -0
- package/xflow/atoms/a5_archive_commit_push_close.py +11 -0
- package/xflow/atoms/a6_pr_create.py +72 -0
- package/xflow/atoms/b1_change_scaffold.py +11 -0
- package/xflow/atoms/b2_status_read.py +16 -0
- package/xflow/atoms/b2b_status_write.py +20 -0
- package/xflow/atoms/b3_status_transition.py +27 -0
- package/xflow/atoms/b4_handoff_scaffold_or_refresh.py +11 -0
- package/xflow/atoms/b5_aha_append.py +11 -0
- package/xflow/atoms/b6_takein_digest.py +11 -0
- package/xflow/atoms/b6b_lookback_digest.py +11 -0
- package/xflow/atoms/b7_change_mission_bind.py +43 -0
- package/xflow/atoms/c1_mem.py +54 -0
- package/xflow/atoms/e6_gate_local_precheck.py +23 -0
- package/xflow/atoms/h1_design_lite_gate.py +74 -0
- package/xflow/atoms/i6a_tdd_run.py +104 -0
- package/xflow/atoms/i6b_tdd_proof_validate.py +165 -0
- package/xflow/atoms/i6c_tdd_quality_review.py +357 -0
- package/xflow/atoms/j1_tests_run.py +129 -0
- package/xflow/atoms/j4a_spec_consistency_structural.py +105 -0
- package/xflow/atoms/j4b_spec_delta_review.py +316 -0
- package/xflow/atoms/j4c_openspec_migration_map.py +121 -0
- package/xflow/atoms/j4d_spec_clarify_gate.py +373 -0
- package/xflow/atoms/j6_cr_review.py +443 -0
- package/xflow/atoms/k1_artifacts_complete_check.py +39 -0
- package/xflow/atoms/k2_merge_snippets_apply.py +11 -0
- package/xflow/atoms/k3_handoff_refresh.py +14 -0
- package/xflow/atoms/k4_mem_lesson_persist.py +65 -0
- package/xflow/atoms/k5_archive_publish.py +21 -0
- package/xflow/atoms/k6_aha_merge.py +42 -0
- package/xflow/atoms/tdd_lib.py +747 -0
- package/xflow/corps/SKILL.md +109 -0
- package/xflow/exec/scripts/set_issue_in_progress.py +159 -0
- package/xflow/goal/SKILL.md +150 -0
- package/xflow/handoff/SKILL.md +95 -0
- package/xflow/handoff/scripts/scaffold_handoff.py +304 -0
- package/xflow/lookback/SKILL.md +66 -0
- package/xflow/lookback/scripts/lookback_digest.py +46 -0
- package/xflow/openissue/scripts/open_issue_flow.py +438 -0
- package/xflow/plan/SKILL.md +38 -0
- package/xflow/ralph/SKILL.md +123 -0
- package/xflow/scripts/check_installed_xflow_skill_sync.sh +44 -0
- package/xflow/scripts/common.py +480 -0
- package/xflow/scripts/doctor.py +278 -0
- package/xflow/scripts/sync_installed_xflow_skill.sh +134 -0
- package/xflow/takein/SKILL.md +81 -0
- package/xflow/takein/scripts/takein_digest.py +88 -0
- package/xflow/yolo/SKILL.md +66 -0
- package/xflow/yolo/scripts/scaffold_specs_change.py +673 -0
- package/xflow/yolo/scripts/yolo_gate.py +312 -0
- 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()
|