specpro-cli 0.1.0__py3-none-any.whl
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.
- specpro_cli/__init__.py +16 -0
- specpro_cli/assets/commands/specpro.analyze.md +1102 -0
- specpro_cli/assets/commands/specpro.checklist.md +335 -0
- specpro_cli/assets/commands/specpro.clarify.md +581 -0
- specpro_cli/assets/commands/specpro.constitution.md +488 -0
- specpro_cli/assets/commands/specpro.feature.md +115 -0
- specpro_cli/assets/commands/specpro.implement.md +1881 -0
- specpro_cli/assets/commands/specpro.manual-test.md +206 -0
- specpro_cli/assets/commands/specpro.plan.md +3284 -0
- specpro_cli/assets/commands/specpro.qc.md +1489 -0
- specpro_cli/assets/commands/specpro.scenarios.md +154 -0
- specpro_cli/assets/commands/specpro.specify.md +1449 -0
- specpro_cli/assets/commands/specpro.status.md +863 -0
- specpro_cli/assets/commands/specpro.tasks.md +1207 -0
- specpro_cli/assets/commands/specpro.test-implement.md +462 -0
- specpro_cli/assets/commands/specpro.test-plan.md +383 -0
- specpro_cli/assets/commands/specpro.user-manual.md +178 -0
- specpro_cli/assets/scripts/bash/check-anti-coupling.sh +293 -0
- specpro_cli/assets/scripts/bash/check-prerequisites.sh +176 -0
- specpro_cli/assets/scripts/bash/common.sh +88 -0
- specpro_cli/assets/scripts/bash/create-new-feature.sh +336 -0
- specpro_cli/assets/scripts/bash/qc-auto-fix.sh +121 -0
- specpro_cli/assets/scripts/bash/setup-plan.sh +60 -0
- specpro_cli/assets/scripts/bash/verify-cumulative-records.sh +203 -0
- specpro_cli/assets/scripts/bash/verify-deliverables-tracked.sh +147 -0
- specpro_cli/assets/scripts/bash/verify-deployment.sh +239 -0
- specpro_cli/assets/scripts/bash/verify-frontmatter-yaml.sh +63 -0
- specpro_cli/assets/scripts/bash/verify-ledger.sh +376 -0
- specpro_cli/assets/scripts/bash/verify-shapes.sh +1082 -0
- specpro_cli/assets/scripts/git-hooks/pre-commit +243 -0
- specpro_cli/assets/scripts/install-git-hooks.sh +67 -0
- specpro_cli/assets/scripts/powershell/check-anti-coupling.ps1 +249 -0
- specpro_cli/assets/scripts/powershell/check-prerequisites.ps1 +148 -0
- specpro_cli/assets/scripts/powershell/common.ps1 +95 -0
- specpro_cli/assets/scripts/powershell/create-new-feature.ps1 +229 -0
- specpro_cli/assets/scripts/powershell/qc-auto-fix.ps1 +110 -0
- specpro_cli/assets/scripts/powershell/setup-plan.ps1 +61 -0
- specpro_cli/assets/scripts/powershell/verify-cumulative-records.ps1 +133 -0
- specpro_cli/assets/scripts/powershell/verify-deliverables-tracked.ps1 +112 -0
- specpro_cli/assets/scripts/powershell/verify-deployment.ps1 +278 -0
- specpro_cli/assets/scripts/powershell/verify-frontmatter-yaml.ps1 +56 -0
- specpro_cli/assets/scripts/powershell/verify-ledger.ps1 +383 -0
- specpro_cli/assets/scripts/powershell/verify-shapes.ps1 +978 -0
- specpro_cli/assets/templates/agent-context-template.md +49 -0
- specpro_cli/assets/templates/assumptions-template.md +248 -0
- specpro_cli/assets/templates/checklist-template.md +40 -0
- specpro_cli/assets/templates/clarifications-template.md +155 -0
- specpro_cli/assets/templates/constitution-template.md +50 -0
- specpro_cli/assets/templates/feature-spec-template.md +66 -0
- specpro_cli/assets/templates/plan-overview-template.md +150 -0
- specpro_cli/assets/templates/plan-template.md +387 -0
- specpro_cli/assets/templates/protocol-golden-bytes-guide.md +195 -0
- specpro_cli/assets/templates/requirements-template.md +356 -0
- specpro_cli/assets/templates/spec-template.md +267 -0
- specpro_cli/assets/templates/tasks-template.md +252 -0
- specpro_cli/assets/templates/test-tasks-template.md +174 -0
- specpro_cli/cli/__init__.py +5 -0
- specpro_cli/cli/cmd_init.py +416 -0
- specpro_cli/cli/cmd_remove.py +122 -0
- specpro_cli/cli/entry.py +181 -0
- specpro_cli/integrations/__init__.py +36 -0
- specpro_cli/integrations/base.py +601 -0
- specpro_cli/integrations/claude/__init__.py +101 -0
- specpro_cli/integrations/copilot/__init__.py +153 -0
- specpro_cli/integrations/cursor_agent/__init__.py +51 -0
- specpro_cli/integrations/gemini/__init__.py +44 -0
- specpro_cli/integrations/opencode/__init__.py +48 -0
- specpro_cli/integrations/qodercli/__init__.py +54 -0
- specpro_cli/integrations/registry.py +88 -0
- specpro_cli/packaged/__init__.py +5 -0
- specpro_cli/packaged/sync.py +106 -0
- specpro_cli-0.1.0.dist-info/METADATA +117 -0
- specpro_cli-0.1.0.dist-info/RECORD +76 -0
- specpro_cli-0.1.0.dist-info/WHEEL +4 -0
- specpro_cli-0.1.0.dist-info/entry_points.txt +2 -0
- specpro_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1489 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Validate specification quality against the checklist's quality criteria. Rebuild the checklist, route every non-passing item to the [specify] ledger queue, and stop. No repair, no loop, no cache.
|
|
3
|
+
writes:
|
|
4
|
+
# This command's write surface: only what it produces AS THE PRODUCER of that
|
|
5
|
+
# (artifact, unit) pair. A write this command makes on a non-producer path is a
|
|
6
|
+
# boundary violation by definition (FR-051) and MUST NOT be declared here.
|
|
7
|
+
# The full ownership map is the UNION of every command's writes: block.
|
|
8
|
+
#
|
|
9
|
+
# ⚠️ **EXACTLY TWO — that IS the ruling, not a summary of it** (FR-058, user ruling
|
|
10
|
+
# 2026-09-19, `ISS-186`). This command produces its checklist, and it routes its
|
|
11
|
+
# findings. It does **NOT** write `spec.md` (the repair belongs to the spec's
|
|
12
|
+
# **producer**, `/specpro-specify`), and does **NOT** write the clarification artifact —
|
|
13
|
+
# that one belongs to `/specpro-specify` too, and holds questions the USER must answer.
|
|
14
|
+
# ⇒ There is no `--auto` path, no repair door, and no third artifact. A future
|
|
15
|
+
# reader who wants to add one here is re-introducing exactly what was removed.
|
|
16
|
+
- artifact: specs/checklists/requirements.md
|
|
17
|
+
unit: "whole file, rebuilt from the template on EVERY run — no history, no cache (FR-060)"
|
|
18
|
+
- artifact: specs/implement_issues.md
|
|
19
|
+
unit: "the [specify] section -> appended ISS-NNN entries for non-passing checks"
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## User Input
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
$ARGUMENTS
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Arguments format: `--scope=[full|incremental] [--quiet]`
|
|
29
|
+
|
|
30
|
+
**Rerun safety — read-only over the artifacts it inspects, and that is now unconditional** ⚠️ [settled 2026-09-13; narrowed to unconditional by the 2026-09-19 ruling]:
|
|
31
|
+
|
|
32
|
+
This command **inspects** `spec.md`, `plan.md`, `tasks.md` and any other artifact under analysis, and it **modifies none of them** — there is no mode that widens this. The shared rerun rule (*detect the artifact; absent → initial, present → incremental; overwriting requires explicit AND re-confirmed intent*) applies to the commands that write those artifacts; this command never triggers its third clause.
|
|
33
|
+
|
|
34
|
+
**Two writes happen on EVERY run, and the list is now EXACTLY two** (`FR-058`):
|
|
35
|
+
|
|
36
|
+
| Write | Why it is not an analyzed artifact |
|
|
37
|
+
|-------|-----------------------------------|
|
|
38
|
+
| `specs/checklists/requirements.md` — **rebuilt from the template on every run** | This command's **own output**: the verdict of *this* run. ⚠️ **Rebuilt, not updated** — see `FR-060`. |
|
|
39
|
+
| `specs/implement_issues.md` — the `[specify]` section gains one entry per **non-passing** check | **Routing, not repair.** A `✗` that lives only in the checklist **has no consumer**, and is indistinguishable from a finding never reported at all (`FR-023`). |
|
|
40
|
+
|
|
41
|
+
⚠️ **What this command deliberately does NOT write, and why it must stay that way** (user ruling 2026-09-19, `ISS-186` — this **reverses the direction** two earlier rounds were heading in):
|
|
42
|
+
|
|
43
|
+
- **NOT `spec.md`.** Repairing the spec belongs to the spec's **producer**, `/specpro-specify`. ⚠️ Earlier rounds tried to make "qc repairs another command's artifact" *authorizable* through a caller-supplied owner parameter (`ISS-38` / `ISS-163` / `T203`). **That direction was rejected**: a command that repairs what it judged has spent the verdict (**Principle III**), and no caller-side authorisation repairs that.
|
|
44
|
+
- **NOT the clarification artifact.** It belongs to `/specpro-specify` — it holds questions **the user** must answer. A criterion failure is not necessarily a question for the user; it is a **finding for the spec's producer**.
|
|
45
|
+
|
|
46
|
+
⇒ **Hence no repair mode, no authorisation parameter, no repair door, and no second path into the write surface.** A defect this command cannot express as a ledger entry belongs in its **report**, not in someone else's artifact.
|
|
47
|
+
|
|
48
|
+
> **Why a shared rule rather than per-command courtesy**: nine of the twelve non-implementation commands already had some protection, but each wrote it its own way, and three had none — not by decision, but because the discipline had no shared carrier. The rule everywhere else is: **detect the artifact; absent → initial, present → incremental; overwriting requires explicit AND re-confirmed intent.** This command's read-only posture means it never triggers the third clause.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
### Scope Resolution 🆕 (FR-063 / T050 · v0.23)
|
|
52
|
+
|
|
53
|
+
1. **作用域判定**: 当前工作目录位于 `specs/fNNN-简称/` 内 ⇒ **feature 作用域**(读写范围 = 本 feature 目录,由 `check-prerequisites.sh` 的作用域感知解析);位于仓库根或 `specs/` 根 ⇒ **母作用域**(读写母规格链)。feature 作用域内 MUST NOT 写母产物——唯一例外:**发现登记**(台账路由,`[specify]`/`[plan]` 分区)。
|
|
54
|
+
2. **新会话首次执行**: 若 `specs/features.md` 存在且含 `active` 行、而用户未指明作用域 ⇒ **询问用户**在母作用域还是某个 feature 内工作,MUST NOT 自行挑选。
|
|
55
|
+
3. 本命令的产物路径随之解析:feature 作用域下落 `<feature 目录>/`,母作用域下落 `specs/`。
|
|
56
|
+
|
|
57
|
+
## Outline
|
|
58
|
+
|
|
59
|
+
> ⚠️ **执行顺序 ≠ 文档顺序 —— 而这是一条声明,不是一条约定**(`T226` / `ISS-194`)
|
|
60
|
+
>
|
|
61
|
+
> 本文档的节次是**按内容组织的**:`## Run`(主流程)出现在它所调用的那些函数行**之前**。
|
|
62
|
+
> ⇒ 执行时 MUST:**先加载全部函数定义**(`## run_qc_check()` · `## PHASE 3` 下的各 `check_*` ·
|
|
63
|
+
> `## register_findings()` · `## Helper Functions`),**然后执行 `## Run`,再按需执行其后各节**。
|
|
64
|
+
>
|
|
65
|
+
> ⚠️ **在写下这一条之前,本文档对执行顺序一言不发** —— 于是两种读法都成立、且都不报错:
|
|
66
|
+
> 逐段顺序执行会在定义之前调用 `run_qc_check` 与 `register_findings`(`command not found`),
|
|
67
|
+
> 而"主流程排在最前"读起来**同样像一个有意的布局**。**一个可以被读成两种意思的节次,
|
|
68
|
+
> 对执行者就是没有节次。**
|
|
69
|
+
>
|
|
70
|
+
> **为什么写死顺序而不是调序**:`Initialize` 之后紧跟主流程是**可读性上的选择**(读者先看到它做什么),
|
|
71
|
+
> 调序要动整份文档的节次;而**顺序一旦写下来,两种读法就只剩一种**。
|
|
72
|
+
|
|
73
|
+
### Parse Arguments
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Parse arguments
|
|
77
|
+
# ⚠️ **There is no repair mode and no owner-authorisation parameter any more** (FR-058, 2026-09-19 ruling).
|
|
78
|
+
# They were not deprecated — they were REMOVED, along with the whole repair path they
|
|
79
|
+
# fed. Adding either back means re-introducing "qc repairs another command's artifact",
|
|
80
|
+
# which the ruling rejected on Principle III grounds (see the note above the table).
|
|
81
|
+
SCOPE="full"
|
|
82
|
+
SHOW_PROGRESS=true
|
|
83
|
+
|
|
84
|
+
# ⚠️ `$ARGUMENTS` is the command framework's **caller-supplied placeholder** (the user's raw
|
|
85
|
+
# input, substituted before this runs). It is declared with an in-place assignment so the
|
|
86
|
+
# loop below reads a DEFINED variable when the snippet is copied or run on its own — which
|
|
87
|
+
# is the failure FR-034 exists to prevent (a snippet that silently iterates over nothing).
|
|
88
|
+
# ⚠️ Not a checker-pleasing no-op: the declaration is the variable's contract, and the
|
|
89
|
+
# `${VAR:=}` form is the contract's own exempt spelling for exactly this kind of input.
|
|
90
|
+
: "${ARGUMENTS:=}"
|
|
91
|
+
for arg in $ARGUMENTS; do
|
|
92
|
+
case $arg in
|
|
93
|
+
--scope=full)
|
|
94
|
+
SCOPE="full"
|
|
95
|
+
;;
|
|
96
|
+
--scope=incremental)
|
|
97
|
+
SCOPE="incremental"
|
|
98
|
+
;;
|
|
99
|
+
--quiet)
|
|
100
|
+
SHOW_PROGRESS=false
|
|
101
|
+
;;
|
|
102
|
+
esac
|
|
103
|
+
done
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Initialize
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Load paths — read-only resolver. Two things this must NOT do:
|
|
110
|
+
# · `source` a JSON-emitting script: `source` cannot consume JSON, and the file it
|
|
111
|
+
# named did not exist — `$SPECS_DIR` came out EMPTY and the five paths below were
|
|
112
|
+
# silently built as "/spec.md", "/checklists/requirements.md", … A checker that
|
|
113
|
+
# resolves its own paths wrongly reports against the wrong place, or against nothing.
|
|
114
|
+
# · call `setup-plan.sh --json` instead: it also reports SPECS_DIR, but it CREATES
|
|
115
|
+
# plan.md from the template when absent — a side effect a checker must not have.
|
|
116
|
+
SPECS_DIR=$(.specpro/scripts/bash/check-prerequisites.sh --json --paths-only 2>/dev/null \
|
|
117
|
+
| sed -n 's/.*"FEATURE_DIR":"\([^"]*\)".*/\1/p')
|
|
118
|
+
if [ -z "$SPECS_DIR" ]; then
|
|
119
|
+
echo "❌ ERROR: could not resolve the specs directory." >&2
|
|
120
|
+
echo " Fix: run this command from the repository root; if it still fails, run /specpro-specify first." >&2
|
|
121
|
+
exit 1
|
|
122
|
+
fi
|
|
123
|
+
FEATURE_SPEC="$SPECS_DIR/spec.md"
|
|
124
|
+
REQUIREMENTS_CHECKLIST="$SPECS_DIR/checklists/requirements.md"
|
|
125
|
+
REQUIREMENTS_TEMPLATE=".specpro/templates/requirements-template.md"
|
|
126
|
+
SPEC_TEMPLATE=".specpro/templates/spec-template.md"
|
|
127
|
+
# ⚠️ A NEW path for this command (FR-059): the `[specify]` section is this command's
|
|
128
|
+
# **routing target**. A non-passing check becomes an entry a named consumer reads.
|
|
129
|
+
IMPL_LEDGER="$SPECS_DIR/implement_issues.md"
|
|
130
|
+
|
|
131
|
+
# Initialize QC state.
|
|
132
|
+
# ⚠️ **Every variable here is written AND read inside one single pass.** The iteration
|
|
133
|
+
# counter, the per-issue consecutive-failure state, the cache and every guard's state are
|
|
134
|
+
# GONE with the main loop (FR-060) — a control flow with no second iteration has no use
|
|
135
|
+
# for them, and *state carried across runs* is exactly what the ruling removed.
|
|
136
|
+
# ⚠️ **The criterion count belongs HERE, beside the other counters — not inside
|
|
137
|
+
# `run_qc_check`** (`T226` / `ISS-194`). It used to be assigned inside that function's body,
|
|
138
|
+
# while the report block **runs earlier in document order** and reads `$TOTAL_ITEMS` ⇒ the
|
|
139
|
+
# summary printed `Checks run: ` with nothing after it. The three counters below were already
|
|
140
|
+
# initialised here; this one was the exception, and **an exception in "where a counter is
|
|
141
|
+
# initialised" is exactly what makes an ordering bug invisible** — four counters that look
|
|
142
|
+
# alike, three of which happen to be correct.
|
|
143
|
+
TOTAL_ITEMS=11 # was 12 until a Content-Quality criterion was retired (2026-09-19, T197)
|
|
144
|
+
PASSED=0
|
|
145
|
+
FAILED=0
|
|
146
|
+
SKIPPED=0
|
|
147
|
+
FAILED_ITEMS=()
|
|
148
|
+
SKIPPED_ITEMS=()
|
|
149
|
+
# "<CHECK_ID>\t<human-readable finding>" — one per non-passing check, built by the check
|
|
150
|
+
# functions themselves via `record_finding`. The id alone cannot fill a ledger entry.
|
|
151
|
+
FINDINGS=()
|
|
152
|
+
# Ledger ids created by THIS run, so the report can name them.
|
|
153
|
+
REGISTERED_IDS=()
|
|
154
|
+
ALL_PASSED=false
|
|
155
|
+
|
|
156
|
+
# 🆕 OPTIMIZATION: Detect incremental validation scope
|
|
157
|
+
# Source of truth: the Lifecycle field (single vocabulary: create|update|delete|
|
|
158
|
+
# processed|deprecated) on the line following each FR — Change Summary verbs are
|
|
159
|
+
# NOT consulted. `-B1` grabs the FR line above each matching Lifecycle line;
|
|
160
|
+
# sub-numbered FRs (e.g. FR-xxxA) are covered by `[A-E]?`.
|
|
161
|
+
if [[ "$SCOPE" = "incremental" ]]; then
|
|
162
|
+
# Extract only new/modified FRs for incremental validation
|
|
163
|
+
ADDED_FRS=$(grep -B1 '\*\*Lifecycle\*\*:.*\[specify:create\]' "$FEATURE_SPEC" 2>/dev/null | grep -oE "FR-[0-9]+[A-E]?" | sort -u)
|
|
164
|
+
MODIFIED_FRS=$(grep -B1 '\*\*Lifecycle\*\*:.*\[specify:update\]' "$FEATURE_SPEC" 2>/dev/null | grep -oE "FR-[0-9]+[A-E]?" | sort -u)
|
|
165
|
+
|
|
166
|
+
# Build list of FRs to validate (only new/modified ones)
|
|
167
|
+
TARGET_FRS="$ADDED_FRS $MODIFIED_FRS"
|
|
168
|
+
|
|
169
|
+
if [[ -n "$TARGET_FRS" ]]; then
|
|
170
|
+
echo "📋 Incremental validation: $(echo "$ADDED_FRS" | wc -w) added, $(echo "$MODIFIED_FRS" | wc -w) modified"
|
|
171
|
+
echo " Validating only affected FRs: $TARGET_FRS"
|
|
172
|
+
else
|
|
173
|
+
echo "ℹ️ No new/modified FRs detected, skipping incremental validation"
|
|
174
|
+
exit 0
|
|
175
|
+
fi
|
|
176
|
+
else
|
|
177
|
+
echo "📋 Full validation: checking all FRs in spec.md"
|
|
178
|
+
TARGET_FRS="all"
|
|
179
|
+
fi
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Run: ONE PASS, then stop ⚠️ [FR-060 / FR-062]
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# ⚠️ **This section used to be an unbounded repair loop, and there is none any more.** The
|
|
188
|
+
# reason is not "simplification" — it is that **the old loop body had nothing left in
|
|
189
|
+
# it that could change its input**.
|
|
190
|
+
#
|
|
191
|
+
# Exactly two actions in that body could alter `spec.md`: the `--auto` repairs inside
|
|
192
|
+
# `run_qc_check`, and the hand-off to `/specpro-clarify` (whose call site said, in its
|
|
193
|
+
# own comment, *"clarify will update spec.md"*). The 2026-09-19 ruling removed BOTH
|
|
194
|
+
# (FR-058 / FR-062) ⇒ the input is fixed and every check is a pure function of it ⇒
|
|
195
|
+
# **iteration 2 could only ever have reproduced iteration 1's verdict.**
|
|
196
|
+
#
|
|
197
|
+
# ⚠️ And the loop did not merely spin — it REPORTED. Its stagnation guard printed
|
|
198
|
+
# "The following issue persists after 3 repair attempts"
|
|
199
|
+
# which is a statement about three repairs that **never happened**; the step it counted
|
|
200
|
+
# said so itself (*"it does not re-run checks and it does not fix anything"*). Removing
|
|
201
|
+
# the loop removes the lie.
|
|
202
|
+
# ⚠️ The one-pass shape is also what makes `FR-060` (rebuild the checklist every run)
|
|
203
|
+
# and `FR-061` (the verdict IS the per-item values) true **by construction**.
|
|
204
|
+
|
|
205
|
+
# 1. Run every criterion exactly once, and rebuild the checklist from the results.
|
|
206
|
+
run_qc_check
|
|
207
|
+
|
|
208
|
+
# 2. Route what did not pass. ⚠️ **Before the verdict, not after.** The exit code must
|
|
209
|
+
# mean "the verdict is final AND every non-passing item has a consumer" — not "the
|
|
210
|
+
# verdict is final, and we hoped somebody would notice" (FR-059).
|
|
211
|
+
register_findings
|
|
212
|
+
|
|
213
|
+
# 3. Verdict and exit code.
|
|
214
|
+
if [[ $FAILED -eq 0 ]]; then
|
|
215
|
+
ALL_PASSED=true
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
echo ""
|
|
219
|
+
if [[ "$ALL_PASSED" = "true" ]]; then
|
|
220
|
+
echo "✅ Quality Validation PASSED"
|
|
221
|
+
else
|
|
222
|
+
echo "❌ Quality Validation FAILED"
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
if [[ "$SHOW_PROGRESS" = "true" ]]; then
|
|
226
|
+
echo ""
|
|
227
|
+
echo "📊 Validation Summary:"
|
|
228
|
+
echo " Checks run: $TOTAL_ITEMS"
|
|
229
|
+
echo " Passed: $PASSED"
|
|
230
|
+
echo " Failed: $FAILED"
|
|
231
|
+
if [[ ${SKIPPED:-0} -gt 0 ]]; then
|
|
232
|
+
echo " Skipped: $SKIPPED (NOT counted as pass or fail — each ℹ️ line above says why)"
|
|
233
|
+
fi
|
|
234
|
+
echo " Checklist: $REQUIREMENTS_CHECKLIST (rebuilt this run)"
|
|
235
|
+
if [[ ${#REGISTERED_IDS[@]} -gt 0 ]]; then
|
|
236
|
+
echo " Routed to [specify]: ${REGISTERED_IDS[*]}"
|
|
237
|
+
fi
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
# ⚠️ **The exit code is the gate's input** — the callers that block on this command read
|
|
241
|
+
# it. It must mean exactly "no criterion failed", which is `FR-061`'s criterion: a
|
|
242
|
+
# `⊘ Skipped` item is **neither** a pass nor a failure and MUST NOT tip this either way.
|
|
243
|
+
if [[ "$ALL_PASSED" = "true" ]]; then
|
|
244
|
+
exit 0
|
|
245
|
+
fi
|
|
246
|
+
exit 1
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## run_qc_check(): Check all quality criteria (ENHANCED)
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
run_qc_check() {
|
|
255
|
+
# Read spec.md
|
|
256
|
+
SPEC_CONTENT=$(cat "$FEATURE_SPEC")
|
|
257
|
+
|
|
258
|
+
# Initialize counters — ⚠️ `TOTAL_ITEMS` is NOT here: it moved to `Initialize` (`T226`),
|
|
259
|
+
# because the report block reads it and that block precedes this function in document order.
|
|
260
|
+
PASSED=0
|
|
261
|
+
FAILED=0
|
|
262
|
+
SKIPPED=0
|
|
263
|
+
FAILED_ITEMS=()
|
|
264
|
+
# ⚠️ Which criteria were SKIPPED, by id — not just how many (T170 / audit finding: the
|
|
265
|
+
# checklist's per-item `**Status**:` values had NO producer, so the three gates that read
|
|
266
|
+
# them saw template placeholders and blocked every run). A count cannot fill twelve cells.
|
|
267
|
+
SKIPPED_ITEMS=()
|
|
268
|
+
|
|
269
|
+
# ⚠️ **There is no loop and no cache, so there is nothing to reset between runs
|
|
270
|
+
# and nothing to read stale state from.** The counters above are the whole of this
|
|
271
|
+
# function's state, and it is rebuilt from zero on every invocation (FR-060).
|
|
272
|
+
|
|
273
|
+
# P0: Content Quality (2 items — a third was retired 2026-09-19, T197)
|
|
274
|
+
check_cq001_implementation_details
|
|
275
|
+
check_cq003_functional_not_technical
|
|
276
|
+
|
|
277
|
+
# P0: Completeness (3 items)
|
|
278
|
+
check_cp001_mandatory_sections
|
|
279
|
+
check_cp002_acceptance_scenarios
|
|
280
|
+
check_cp003_scenarios_functional
|
|
281
|
+
|
|
282
|
+
# P1: Consistency (4 items)
|
|
283
|
+
check_cs001_us_consistency
|
|
284
|
+
check_cs002_fr_consistency
|
|
285
|
+
check_cs003_us_fr_mapping
|
|
286
|
+
check_cs004_reference_integrity
|
|
287
|
+
|
|
288
|
+
# P1: Traceability (2 items)
|
|
289
|
+
check_tr001_constitution_constraints
|
|
290
|
+
check_tr002_external_dependencies
|
|
291
|
+
|
|
292
|
+
# Rebuild the checklist from THIS run's results. ⚠️ Last, so that every check has
|
|
293
|
+
# already contributed to PASSED / FAILED / SKIPPED / SKIPPED_ITEMS.
|
|
294
|
+
update_checklist
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## 🆕 PHASE 3: Enhanced Check Functions with Detailed Error Messages
|
|
301
|
+
|
|
302
|
+
### check_cq001_implementation_details() - ENHANCED
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
check_cq001_implementation_details() {
|
|
306
|
+
# ⚠️ **WORD BOUNDARIES ARE REQUIRED, not cosmetic** (T094/ISS-29): unwrapped, every one of
|
|
307
|
+
# these matches inside unrelated words — `class` in "classify", `def` in "definition",
|
|
308
|
+
# `interface` in "the interface between", `private` in a path segment. The check then reports
|
|
309
|
+
# **implementation details that are not there**. (`\bSQL\b` deliberately still matches the
|
|
310
|
+
# bare token while NOT matching `PostgreSQL` / `MySQL` — the boundary is between `Y` and `S`,
|
|
311
|
+
# both word characters.) ⚠️ The half that made these false positives INVISIBLE was the empty
|
|
312
|
+
# scan container — both are fixed together, because fixing either alone is worse than neither.
|
|
313
|
+
local fail_keywords="\bReact\b|\bVue\b|\bAngular\b|\bSQL\b|\bPostgreSQL\b|\bMongoDB\b|\bMySQL\b|\bRedis\b|\bREST\b|\bGraphQL\b|\bHTTP\b|async/await|\bawait\b|\bcoroutine\b|\bclass\b|\bfunction\b|\bdef\b|\bpublic\b|\bprivate\b|\binterface\b|\babstract\b|\bextends\b|\bimplements\b"
|
|
314
|
+
|
|
315
|
+
# ⚠️ **The FR-item pattern lives in ONE variable and BOTH paths use it** (T094/ISS-91):
|
|
316
|
+
# the template emits an INDENTED bold list item — ` - **FR-XXX**: [Title]` — never
|
|
317
|
+
# `#### FR-xxx`, which appears **nowhere** in `templates/spec-template.md`. The old
|
|
318
|
+
# container therefore matched 0 rows on every spec built from the template, `found` was
|
|
319
|
+
# always empty, and the check reported PASS by construction (ISS-83's shape: the extractor
|
|
320
|
+
# finds nothing ⇒ an empty run reads as green). Fixing only the main path would leave the
|
|
321
|
+
# incremental branch below scanning the same wrong container — that is why this is a
|
|
322
|
+
# variable, not two literals.
|
|
323
|
+
local FR_ITEM='^[[:space:]]*- \*\*FR-[0-9]+[A-E]?\*\*:'
|
|
324
|
+
|
|
325
|
+
# 🆕 PHASE 3: Filter by TARGET_FRS if incremental
|
|
326
|
+
local search_scope="$FEATURE_SPEC"
|
|
327
|
+
if [[ "$TARGET_FRS" != "all" ]] && [[ -n "$TARGET_FRS" ]]; then
|
|
328
|
+
# Extract only the relevant FRs from spec.md
|
|
329
|
+
local temp_spec=$(mktemp)
|
|
330
|
+
for fr in $TARGET_FRS; do
|
|
331
|
+
grep -A 10 -E "^[[:space:]]*- \*\*$fr\*\*:" "$FEATURE_SPEC" >> "$temp_spec" || true
|
|
332
|
+
done
|
|
333
|
+
search_scope="$temp_spec"
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
# ⚠️ **Reachability guard** (T093/FR-038): with the container empty the `found` below is
|
|
337
|
+
# empty too, and the check reports PASS — an **unfired predicate**, which is
|
|
338
|
+
# indistinguishable in a pass/fail report from a check that ran and found nothing wrong
|
|
339
|
+
# (形态 6). Counted as SKIPPED, never PASSED.
|
|
340
|
+
# ⚠️ **`|| echo 0` is WRONG here** and was the cause of a silent corruption: `grep -c`
|
|
341
|
+
# prints `0` AND exits 1, so that idiom yields the two-line string `"0\n0"` — and
|
|
342
|
+
# `[[ "0\n0" -eq 0 ]]` is a **syntax error**, so the guard never fires. `|| true`
|
|
343
|
+
# neutralises the exit code WITHOUT appending text; `${scanned_rows:=0}` then covers
|
|
344
|
+
# the empty case.
|
|
345
|
+
local scanned_rows=$(grep -cE "$FR_ITEM" "$search_scope" 2>/dev/null || true)
|
|
346
|
+
: "${scanned_rows:=0}"
|
|
347
|
+
if [[ "$scanned_rows" -eq 0 ]]; then
|
|
348
|
+
echo " ⊘ CQ001: skipped — the scan container matched 0 rows, so nothing was read"
|
|
349
|
+
SKIPPED_ITEMS+=("CQ001")
|
|
350
|
+
echo " ⚠️ An unfired predicate is NOT a pass. The container is likely mis-specified."
|
|
351
|
+
SKIPPED=$((SKIPPED + 1))
|
|
352
|
+
return
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
# Search for implementation details in FRs
|
|
356
|
+
local found=$(grep -E "$FR_ITEM" "$search_scope" | grep -iE "($fail_keywords)" || true)
|
|
357
|
+
|
|
358
|
+
# ⚠️ **A residual false positive that word boundaries do NOT fix** — the third one named for
|
|
359
|
+
# this check (ISS-29's list: `defect`, `classify`, `private/spec-kit-main`). `\bprivate\b`
|
|
360
|
+
# still matches `private/spec-kit-main`: the segment IS the whole word, and `/` is a
|
|
361
|
+
# non-word character, so it satisfies both boundaries. **Word boundaries fix two of the
|
|
362
|
+
# three, not three** — a path is not an implementation-detail declaration, so matches whose
|
|
363
|
+
# keyword sits inside a path are dropped here.
|
|
364
|
+
# ⚠️ **A remaining limit, stated rather than papered over**: a legitimate English use of a
|
|
365
|
+
# listed word ("the interface between two layers") still matches — that is inherent to a
|
|
366
|
+
# keyword scan and is why this check's verdict is an indicator, not a proof. It belongs to
|
|
367
|
+
# the coverage declaration, not to a cleverer regex.
|
|
368
|
+
local path_like='(private|public|interface|class|def|function|abstract|extends|implements)'
|
|
369
|
+
found=$(printf '%s\n' "$found" | grep -vE "/${path_like}\b|\b${path_like}/" || true)
|
|
370
|
+
|
|
371
|
+
if [[ -n "$found" ]]; then
|
|
372
|
+
echo " ✗ CQ001: No implementation details"
|
|
373
|
+
|
|
374
|
+
# 🆕 PHASE 3: Detailed error messages
|
|
375
|
+
local count=$(echo "$found" | wc -l)
|
|
376
|
+
echo " Found: $count FR(s) with implementation details"
|
|
377
|
+
|
|
378
|
+
# 🆕 PHASE 3: Show specific FRs with issues
|
|
379
|
+
echo " Affected FRs:"
|
|
380
|
+
echo "$found" | while IFS= read -r line; do
|
|
381
|
+
local fr_id=$(echo "$line" | grep -oE "FR-[0-9]+" | head -1)
|
|
382
|
+
local keyword=$(echo "$line" | grep -oiE "($fail_keywords)" | head -1)
|
|
383
|
+
echo " - $fr_id: contains '$keyword'"
|
|
384
|
+
done
|
|
385
|
+
|
|
386
|
+
# 🆕 PHASE 3: Fix suggestion
|
|
387
|
+
echo " 💡 Suggestion: Remove technical keywords and describe functionality instead"
|
|
388
|
+
echo " Example: 'System provides responsive web interface' (not 'System uses React')"
|
|
389
|
+
|
|
390
|
+
# ⚠️ **There used to be a repair here, behind an owner-authorisation gate.** Both are gone
|
|
391
|
+
# (`FR-058`, 2026-09-19 ruling): this criterion's subject is `spec.md`, which is NOT
|
|
392
|
+
# in this command's write face — and earlier rounds tried to make that repairable by
|
|
393
|
+
# *authorising* it through a caller-supplied owner. **The ruling rejected the
|
|
394
|
+
# direction, not the mechanism**: a command that repairs what it judged has spent the
|
|
395
|
+
# verdict (Principle III), and no parameter repairs that.
|
|
396
|
+
# ⇒ What this check does now is **report**, and `register_findings()` routes it.
|
|
397
|
+
echo " Action: reported — /specpro-specify is the producer that repairs spec.md"
|
|
398
|
+
FAILED=$((FAILED + 1))
|
|
399
|
+
FAILED_ITEMS+=("CQ001")
|
|
400
|
+
record_finding "CQ001" "Some requirements describe HOW instead of WHAT"
|
|
401
|
+
else
|
|
402
|
+
echo " ✓ CQ001: No implementation details"
|
|
403
|
+
PASSED=$((PASSED + 1))
|
|
404
|
+
fi
|
|
405
|
+
|
|
406
|
+
# Cleanup temp file if created
|
|
407
|
+
if [[ "$search_scope" != "$FEATURE_SPEC" ]] && [[ -f "$search_scope" ]]; then
|
|
408
|
+
rm "$search_scope"
|
|
409
|
+
fi
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### ⚠️ RETIRED: the "no technical performance metrics in spec.md" criterion (T197)
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
# ⚠️ **This criterion was RETIRED (2026-09-19, `T197`), and the retirement is the point.**
|
|
417
|
+
#
|
|
418
|
+
# Its premise was false: **a performance metric is a REQUIREMENT, not an implementation
|
|
419
|
+
# detail.** "How you achieve it" is the implementation detail — that is `CQ001`'s and
|
|
420
|
+
# `CQ003`'s territory. Its pattern matched the WHOLE of spec.md with no section scoping,
|
|
421
|
+
# so a single legitimate `MUST respond within 200ms` in an FR marked the checklist
|
|
422
|
+
# BLOCKED — and a blocked checklist stops plan / tasks / implement.
|
|
423
|
+
#
|
|
424
|
+
# ⚠️ **It forbade the very input the rest of the toolchain reads**: plan's Technical
|
|
425
|
+
# Context Performance Goals, contracts' Performance Requirements, analyze's Pass H loss
|
|
426
|
+
# detection, and test-plan's `Performance:` fields all source from quantified FRs. The
|
|
427
|
+
# criterion was the only thing in the chain that said such an FR must not exist.
|
|
428
|
+
#
|
|
429
|
+
# ⚠️ **It was NOT re-scoped.** A narrower version ("metrics must not be bound to an
|
|
430
|
+
# implementation method") would have duplicated `CQ003`, which already owns exactly that.
|
|
431
|
+
#
|
|
432
|
+
# ⚠️ **And its clarification loop did not converge**: it reported to the [specify] ledger queue
|
|
433
|
+
# without deleting, while the repo's rule is that a resolved clarification folds back
|
|
434
|
+
# into spec.md as a requirement entry — which re-trips the criterion on the next run.
|
|
435
|
+
#
|
|
436
|
+
# ⚠️ Its helper `report_performance_metrics()` is retired with it — and so is
|
|
437
|
+
# `report_scenario_performance_metrics()`, which a previous revision of this very note said
|
|
438
|
+
# **STAYS** (`T240` / `ISS-213`). ⚠️ **That sentence was false the moment it was written**:
|
|
439
|
+
# the function is not defined anywhere in this file, while `check_cp003_scenarios_functional`
|
|
440
|
+
# kept calling it for another round. **Its rationale — not its body — is what actually stays**,
|
|
441
|
+
# and it now lives at that call site's replacement (the `⚠️ This check REPORTS; it never
|
|
442
|
+
# removes` note inside `check_cp003`): scenarios are a *different subject* from the whole
|
|
443
|
+
# document, so a literal-match delete pass stays the wrong instrument for them too.
|
|
444
|
+
# ⇒ **The lesson is the one this file keeps applying**: a promise in prose ("it STAYS") is not
|
|
445
|
+
# a carrier — the definition is, and when the two disagree the prose is what misleads.
|
|
446
|
+
#
|
|
447
|
+
# ⚠️ **Known gap, registered not hidden** (see the ledger): the shape "use <method> to
|
|
448
|
+
# reach <metric>" is matched by NEITHER `CQ001` nor `CQ003` as they stand. Retiring this
|
|
449
|
+
# criterion therefore leaves that one shape uncaught — measured, not assumed.
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
# ⚠️ **A function used to be defined here — `show_failed_items_summary()`. It is GONE,
|
|
454
|
+
# and the reason it is worth a note is that it outlived its caller by one round.**
|
|
455
|
+
#
|
|
456
|
+
# Its only call site was inside the repair loop, which was removed when the loop went
|
|
457
|
+
# (FR-060). The definition stayed behind: a complete, working function that **nothing
|
|
458
|
+
# ever called**. Three things made that state easy to miss, and all three are the same
|
|
459
|
+
# shape this repository keeps finding:
|
|
460
|
+
# * a function with no caller produces **no error** — it is silently dead, not broken;
|
|
461
|
+
# * a `grep` for the function name returns a hit — so "it exists" reads as "it works";
|
|
462
|
+
# * and its body had gone **stale relative to the checks it summarised** (it listed
|
|
463
|
+
# `CS001`–`CS003` under Consistency and had no line for `CS004`, which exists).
|
|
464
|
+
#
|
|
465
|
+
# ⇒ **If you are looking for the failed-items summary: it went with the loop.** The
|
|
466
|
+
# report in the single-pass run prints the counts and the routed ids directly; a
|
|
467
|
+
# category-by-category breakdown was the loop's progress display, and with a single
|
|
468
|
+
# pass there is no progress to display.
|
|
469
|
+
#
|
|
470
|
+
# ⚠️ **Removed, not deprecated** — a commented-out copy would be a second definition
|
|
471
|
+
# waiting to be picked up by whoever greps next.
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### check_cq003_functional_not_technical()
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
check_cq003_functional_not_technical() {
|
|
478
|
+
# Check for technical "HOW" language vs functional "WHAT"
|
|
479
|
+
# Patterns must describe a HOW (a named technology, library or algorithm chosen to
|
|
480
|
+
# do the work), not merely a means-phrase. A bare `implement ... using <noun>` also
|
|
481
|
+
# matches legitimate protocol/business wording ("negotiation using message type N"),
|
|
482
|
+
# which is a WHAT and must not be flagged. Hence the object is constrained to a
|
|
483
|
+
# technology-shaped token.
|
|
484
|
+
#
|
|
485
|
+
# ⚠️ **`use <method> to <outcome>` — the method-bound-to-a-metric shape** (T201,
|
|
486
|
+
# 2026-09-19). This criterion is where the retired whole-document performance-metric ban
|
|
487
|
+
# handed its remaining duty: a numeric target is a REQUIREMENT, but **naming the method
|
|
488
|
+
# that attains it is an implementation detail** — "how you achieve it" is exactly what
|
|
489
|
+
# this criterion owns. Measured before the pattern was added: `System MUST use async I/O
|
|
490
|
+
# to reach 60 FPS` matched **none** of the frames above and none of `CQ001`'s keywords,
|
|
491
|
+
# so the shape had **no checker at all**.
|
|
492
|
+
#
|
|
493
|
+
# ⚠️ **Constrained by SHAPE, not by a verb list.** The obvious frame — `use X to
|
|
494
|
+
# (reach|achieve|hit|…)` — is an enumeration of outcome verbs, and an enumeration
|
|
495
|
+
# narrower than the ways outcomes get written is the shape this repository has paid for
|
|
496
|
+
# repeatedly (`ISS-84` rejected exactly that move). The reliable feature is on the
|
|
497
|
+
# METHOD side: a named method carries a slash (`async I/O`, `HTTP/2`, `read/write`),
|
|
498
|
+
# while a user-facing control does not ("use the Connect button to start"). So the
|
|
499
|
+
# pattern requires a slash-bearing token and leaves the verb open.
|
|
500
|
+
#
|
|
501
|
+
# ⚠️ **Its honest limit, stated rather than implied**: a method spelled without a slash
|
|
502
|
+
# ("a thread pool") is NOT caught. That is the same remainder this criterion already
|
|
503
|
+
# declares — its template entry reads `**Validation**: Semi-automated` — and the
|
|
504
|
+
# remainder is human review, not a quiet pass.
|
|
505
|
+
local technical_patterns=(
|
|
506
|
+
"use[[:space:]]+[A-Za-z0-9_.-]+[[:space:]]+to[[:space:]]+implement"
|
|
507
|
+
"implement[^.]{0,40}using[[:space:]]+(React|Vue|Angular|Ktor|SQLDelight|SQLite|PostgreSQL|MySQL|MongoDB|Redis|GraphQL|Istanbul|NYC|Docker|Kubernetes|Hibernate|Express|Django|Flask|Spring)|implement[^.]{0,40}via[[:space:]]+[A-Za-z0-9_.-]+[[:space:]]+(algorithm|library|framework|toolkit)"
|
|
508
|
+
"achieve[^.]{0,30}by[[:space:]]+[A-Za-z0-9_.-]+[[:space:]]+(algorithm|library|framework|toolkit)"
|
|
509
|
+
"via[[:space:]]+[A-Za-z0-9_.-]+[[:space:]]+algorithm"
|
|
510
|
+
"utiliz(e|ing)[[:space:]]+[A-Za-z0-9_.-]+[[:space:]]+library"
|
|
511
|
+
"use[^.]{0,40}/[A-Za-z0-9_.-]*[[:space:]]+to[[:space:]]+[a-z]+"
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
local found=false
|
|
515
|
+
for pattern in "${technical_patterns[@]}"; do
|
|
516
|
+
if grep -qiE "$pattern" "$FEATURE_SPEC"; then
|
|
517
|
+
found=true
|
|
518
|
+
break
|
|
519
|
+
fi
|
|
520
|
+
done
|
|
521
|
+
|
|
522
|
+
if [[ "$found" == "true" ]]; then
|
|
523
|
+
echo " ✗ CQ003: Requirements are functional, not technical"
|
|
524
|
+
echo " Found technical implementation language in FRs"
|
|
525
|
+
|
|
526
|
+
record_finding "CQ003" "Some requirements describe HOW instead of WHAT"
|
|
527
|
+
FAILED=$((FAILED + 1))
|
|
528
|
+
FAILED_ITEMS+=("CQ003")
|
|
529
|
+
else
|
|
530
|
+
echo " ✓ CQ003: Requirements are functional, not technical"
|
|
531
|
+
PASSED=$((PASSED + 1))
|
|
532
|
+
fi
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### check_cp001_mandatory_sections()
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
check_cp001_mandatory_sections() {
|
|
540
|
+
# ⚠️ **FR-041 — the list is READ FROM THE ARTIFACT'S TEMPLATE, not held here.**
|
|
541
|
+
# This function used to carry `local required_sections=("## Overview" "## User Scenarios &
|
|
542
|
+
# Testing" "## Requirements")`. Measured, that array had two disagreements behind it: the
|
|
543
|
+
# checklist template declared `Functional Requirements` (a sub-heading) while the array
|
|
544
|
+
# named `## Requirements` (its container), and `templates/spec-template.md` — the file
|
|
545
|
+
# that actually PRODUCES `spec.md` — defined **no `## Overview` at all**. So the criterion
|
|
546
|
+
# demanded a section no producer emits, and every spec generated from the template failed
|
|
547
|
+
# against something it was never given the means to satisfy.
|
|
548
|
+
# **A criterion with two sources has no source**; the template's own `*(mandatory)*`
|
|
549
|
+
# markers are the one that counts, and this is where they are read.
|
|
550
|
+
local required_sections=()
|
|
551
|
+
while IFS= read -r s; do
|
|
552
|
+
[[ -n "$s" ]] && required_sections+=("$s")
|
|
553
|
+
done < <(grep -E '^## .*\*\(mandatory\)\*' "$SPEC_TEMPLATE" 2>/dev/null \
|
|
554
|
+
| sed 's/\*([^)]*)\*.*$//; s/[[:space:]]*$//' || true)
|
|
555
|
+
|
|
556
|
+
# ⚠️ **No declared set ⇒ the defect is HERE, not in the artifact** (FR-041). A criterion
|
|
557
|
+
# whose target the template never defines cannot be failed against a spec — and the empty
|
|
558
|
+
# `missing` list below must not be allowed to fall through as a pass (形态 6).
|
|
559
|
+
if [[ ${#required_sections[@]} -eq 0 ]]; then
|
|
560
|
+
echo " ⊘ CP001: not executable — the artifact template declares no mandatory sections"
|
|
561
|
+
SKIPPED_ITEMS+=("CP001")
|
|
562
|
+
echo " Checked: $SPEC_TEMPLATE"
|
|
563
|
+
echo " ⚠️ This is a defect in the CHECK, not in the spec (FR-041): with nothing declared"
|
|
564
|
+
echo " there is nothing to judge, and an unfired check is not a pass."
|
|
565
|
+
SKIPPED=$((SKIPPED + 1))
|
|
566
|
+
return
|
|
567
|
+
fi
|
|
568
|
+
|
|
569
|
+
local missing=()
|
|
570
|
+
for section in "${required_sections[@]}"; do
|
|
571
|
+
# Line-start match, deliberately NOT `grep -F`: a fixed-string match would also accept a
|
|
572
|
+
# *mention* of the heading inside prose, which is the "presence of a marker is not the
|
|
573
|
+
# requirement being met" defect this file keeps closing (FR-039).
|
|
574
|
+
if ! grep -q "^$section" "$FEATURE_SPEC"; then
|
|
575
|
+
missing+=("$section")
|
|
576
|
+
fi
|
|
577
|
+
done
|
|
578
|
+
|
|
579
|
+
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
580
|
+
echo " ✗ CP001: All mandatory sections completed"
|
|
581
|
+
echo " Missing: ${missing[*]}"
|
|
582
|
+
|
|
583
|
+
# Manual fix required
|
|
584
|
+
record_finding "CP001" "Missing mandatory sections: ${missing[*]}"
|
|
585
|
+
FAILED=$((FAILED + 1))
|
|
586
|
+
FAILED_ITEMS+=("CP001")
|
|
587
|
+
else
|
|
588
|
+
echo " ✓ CP001: All mandatory sections completed"
|
|
589
|
+
PASSED=$((PASSED + 1))
|
|
590
|
+
fi
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### check_cp002_acceptance_scenarios()
|
|
595
|
+
|
|
596
|
+
```bash
|
|
597
|
+
check_cp002_acceptance_scenarios() {
|
|
598
|
+
# ⚠️ **The User-Story heading pattern, in ONE variable — both uses below read it**
|
|
599
|
+
# (T094/ISS-63). The template emits `### US1 - [Brief Title] (Priority: P1)`
|
|
600
|
+
# (`templates/spec-template.md`), never `### User Story` — so the old pattern matched 0
|
|
601
|
+
# rows on every generated spec, the count was always 0, and `0 != 0` is false ⇒ the check
|
|
602
|
+
# took its **else** branch and reported PASS. ⚠️ Both uses must move together: the count
|
|
603
|
+
# AND the line-number lookup below (that one sits inside `if [[ $us_count -gt 0 ]]`, so a
|
|
604
|
+
# zero count meant it never even executed).
|
|
605
|
+
local US_HEADING='^### US[0-9]+ '
|
|
606
|
+
|
|
607
|
+
# Count User Stories
|
|
608
|
+
# ⚠️ `|| echo "0"` was WRONG (fixed with T093): `grep -c` prints `0` AND exits 1, so it
|
|
609
|
+
# produced the two-line string `"0\n0"` — every `[[ $us_count -op … ]]` below was then a
|
|
610
|
+
# **syntax error**, the `if` fell through to its else branch, and the check reported
|
|
611
|
+
# PASS **because of a shell error**, not because it decided anything. `|| true` keeps the
|
|
612
|
+
# exit code quiet without appending text.
|
|
613
|
+
local us_count=$(grep -cE "$US_HEADING" "$FEATURE_SPEC" || true)
|
|
614
|
+
: "${us_count:=0}"
|
|
615
|
+
|
|
616
|
+
# ⚠️ **Reachability guard** (T093/FR-038): with `us_count = 0` the comparison at the end
|
|
617
|
+
# is vacuously true (`0 != 0` is false ⇒ the else branch ⇒ PASS) — the loop below never
|
|
618
|
+
# runs, so the check decides nothing while reporting a pass. Counted as SKIPPED.
|
|
619
|
+
# ⚠️ The pattern is also wrong for this template: it emits `### US1 - …`, not
|
|
620
|
+
# `### User Story` — so this guard fires on every spec built from the template. That is
|
|
621
|
+
# T094's fix; the guard makes it VISIBLE instead of silent.
|
|
622
|
+
if [[ "${us_count:-0}" -eq 0 ]]; then
|
|
623
|
+
echo " ⊘ CP002: skipped — 0 User Stories matched by the scan pattern"
|
|
624
|
+
SKIPPED_ITEMS+=("CP002")
|
|
625
|
+
echo " ⚠️ Either the spec has none, or the pattern does not match the template's form."
|
|
626
|
+
echo " A count that can never be non-zero decides nothing — it is NOT a pass."
|
|
627
|
+
SKIPPED=$((SKIPPED + 1))
|
|
628
|
+
return
|
|
629
|
+
fi
|
|
630
|
+
|
|
631
|
+
# Count User Stories with Acceptance Scenarios (per-US block scan)
|
|
632
|
+
# Format contract: scenario line is a bold text line `**Acceptance Scenarios**:`
|
|
633
|
+
local us_with_scenarios=0
|
|
634
|
+
if [[ $us_count -gt 0 ]]; then
|
|
635
|
+
local line_nums=($(grep -nE "$US_HEADING" "$FEATURE_SPEC" | cut -d: -f1))
|
|
636
|
+
local total_lines=$(wc -l < "$FEATURE_SPEC" | tr -d ' ')
|
|
637
|
+
for i in "${!line_nums[@]}"; do
|
|
638
|
+
local start=${line_nums[$i]}
|
|
639
|
+
local end=$total_lines
|
|
640
|
+
if [[ $((i + 1)) -lt ${#line_nums[@]} ]]; then
|
|
641
|
+
end=$((${line_nums[$((i + 1))]} - 1))
|
|
642
|
+
fi
|
|
643
|
+
if sed -n "${start},${end}p" "$FEATURE_SPEC" | grep -q '^\*\*Acceptance Scenarios\*\*:'; then
|
|
644
|
+
us_with_scenarios=$((us_with_scenarios + 1))
|
|
645
|
+
fi
|
|
646
|
+
done
|
|
647
|
+
fi
|
|
648
|
+
|
|
649
|
+
if [[ $us_count -ne $us_with_scenarios ]]; then
|
|
650
|
+
echo " ✗ CP002: Every User Story has Acceptance Scenarios"
|
|
651
|
+
echo " Missing: $((us_count - us_with_scenarios)) US without scenarios"
|
|
652
|
+
|
|
653
|
+
record_finding "CP002" "Some User Stories lack Acceptance Scenarios"
|
|
654
|
+
FAILED=$((FAILED + 1))
|
|
655
|
+
FAILED_ITEMS+=("CP002")
|
|
656
|
+
else
|
|
657
|
+
echo " ✓ CP002: Every User Story has Acceptance Scenarios"
|
|
658
|
+
PASSED=$((PASSED + 1))
|
|
659
|
+
fi
|
|
660
|
+
}
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### check_cp003_scenarios_functional()
|
|
664
|
+
|
|
665
|
+
```bash
|
|
666
|
+
check_cp003_scenarios_functional() {
|
|
667
|
+
# Check Acceptance Scenarios for performance metrics
|
|
668
|
+
# NOTE: ERE has no \d — use [0-9]+; anchor the bold literal with escaped asterisks
|
|
669
|
+
#
|
|
670
|
+
# Scope = the scenario block proper, bounded STRUCTURALLY (a line that is entirely
|
|
671
|
+
# bold, e.g. the next `**Lifecycle**:` / `**Related Requirements**:` marker, ends it).
|
|
672
|
+
# A fixed look-ahead window (the former `grep -A 50`) does not track the block: it
|
|
673
|
+
# overruns into whatever follows — a separate "performance scenarios" subsection, for
|
|
674
|
+
# instance — and reports that section's metrics as if they were scenario text.
|
|
675
|
+
local found=$(awk '
|
|
676
|
+
/^\*\*Acceptance Scenarios\*\*:/ { inblock=1; next }
|
|
677
|
+
inblock && /^\*\*[^*]+\*\*[[:space:]]*:/ { inblock=0; next }
|
|
678
|
+
inblock { print }
|
|
679
|
+
' "$FEATURE_SPEC" | grep -E "within [0-9]+ ?ms|under [0-9]+ ?ms|[0-9]+ FPS" || true)
|
|
680
|
+
|
|
681
|
+
if [[ -n "$found" ]]; then
|
|
682
|
+
echo " ✗ CP003: Acceptance Scenarios are functional (not technical)"
|
|
683
|
+
echo " Found: $(echo "$found" | wc -l) scenarios with performance metrics"
|
|
684
|
+
|
|
685
|
+
# ⚠️ **This check REPORTS; it never removes.** A delete pass driven by a literal match
|
|
686
|
+
# is the wrong instrument for this class — the match cannot tell a *target* (belongs in
|
|
687
|
+
# plan.md) from a *requirement* (restate it functionally), and one of the patterns
|
|
688
|
+
# (`[0-9]+ ?FPS`) is unanchored. ⚠️ **Do not add an auto-fix here.**
|
|
689
|
+
#
|
|
690
|
+
# ⚠️ **The reporting itself is NOT done here** — it is `register_findings`, which runs
|
|
691
|
+
# once after every check has contributed to `FAILED_ITEMS` (`FR-059`). That is why this
|
|
692
|
+
# block only records the verdict and prints the action line.
|
|
693
|
+
#
|
|
694
|
+
# ⚠️ **And `record_finding` below is load-bearing, not decoration** (`T240` verification):
|
|
695
|
+
# `register_findings` iterates **`FINDINGS`**, not `FAILED_ITEMS` — so a check that
|
|
696
|
+
# increments the counter without recording a finding gets a `✗` in the checklist and a
|
|
697
|
+
# **FAILED verdict**, while nothing reaches the ledger. This check was the **only one of
|
|
698
|
+
# twelve** missing the call (measured: `check_cq001`…`check_tr002` each call it; `cp003`
|
|
699
|
+
# called it zero times) ⇒ its own `Action:` line — "reported to the `[specify]` ledger
|
|
700
|
+
# queue" — was **false on every failure**, and `FINDINGS`' own comment ("the id alone
|
|
701
|
+
# cannot fill a ledger entry") explains why the missing call could not be papered over
|
|
702
|
+
# downstream. ⚠️ **Do not remove it**; a second check losing this call is what the sweep
|
|
703
|
+
# above is for.
|
|
704
|
+
record_finding "CP003" "Some Acceptance Scenarios carry performance metrics (technical, not functional)"
|
|
705
|
+
#
|
|
706
|
+
# ⚠️ **A call to `report_scenario_performance_metrics` used to sit on this line, and it
|
|
707
|
+
# was REMOVED** (`T240` / `ISS-213`): the function is not defined in this file, and it
|
|
708
|
+
# cannot be restored as it was — it reported by writing `clarifications.md`, which
|
|
709
|
+
# `FR-058` removed from this command's write surface (exactly two: its checklist, and
|
|
710
|
+
# its `[specify]` entries). Its in-block scan already lives inline above. ⚠️ **Do not
|
|
711
|
+
# re-add the call**: an undefined function on a check's failure path prints
|
|
712
|
+
# `command not found` and — this file has no `set -e` — execution continues, so the
|
|
713
|
+
# verdict and the routing both still happen, and the only thing the call ever produced
|
|
714
|
+
# was a line of stderr and a false impression that a report had been made *here*.
|
|
715
|
+
echo " Action: reported to the [specify] ledger queue (NOT auto-removed)"
|
|
716
|
+
FAILED=$((FAILED + 1))
|
|
717
|
+
FAILED_ITEMS+=("CP003")
|
|
718
|
+
else
|
|
719
|
+
echo " ✓ CP003: Acceptance Scenarios are functional"
|
|
720
|
+
PASSED=$((PASSED + 1))
|
|
721
|
+
fi
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
### check_cs001_us_consistency()
|
|
726
|
+
|
|
727
|
+
```bash
|
|
728
|
+
check_cs001_us_consistency() {
|
|
729
|
+
# Check for contradictory User Stories
|
|
730
|
+
# Simplified: hardcoded keyword-pair check only — semantic consistency
|
|
731
|
+
# is owned by /specpro-analyze. Do not extend here.
|
|
732
|
+
local found=false
|
|
733
|
+
|
|
734
|
+
# Example: Check for "offline" vs "always-on" contradictions
|
|
735
|
+
if grep -qi "offline.*mode" "$FEATURE_SPEC" && grep -qi "always.*on.*connection" "$FEATURE_SPEC"; then
|
|
736
|
+
found=true
|
|
737
|
+
fi
|
|
738
|
+
|
|
739
|
+
if [[ "$found" == "true" ]]; then
|
|
740
|
+
# ⚠️ **The verdict names what was MEASURED, not what it would like to conclude** (T170 /
|
|
741
|
+
# audit finding: "用一对固定短语的命中与否,断言一个全称结论"). The criterion probes ONE
|
|
742
|
+
# keyword pair; a `✓` that read "internally consistent" asserted a universal property
|
|
743
|
+
# nothing here checks. The `scope:` line below said so — the verdict line contradicted it.
|
|
744
|
+
echo " ✗ CS001: the keyword-pair probe MATCHED — possible contradiction"
|
|
745
|
+
echo " Possible contradiction detected"
|
|
746
|
+
|
|
747
|
+
# Manual intervention required
|
|
748
|
+
record_finding "CS001" "Possible contradiction in User Stories"
|
|
749
|
+
FAILED=$((FAILED + 1))
|
|
750
|
+
FAILED_ITEMS+=("CS001")
|
|
751
|
+
else
|
|
752
|
+
echo " ✓ CS001: no contradiction detected by the keyword-pair probe"
|
|
753
|
+
# ⚠️ **Scope stated where the verdict is read** (T097 / FR-038, handed over by T123).
|
|
754
|
+
# This check is a **keyword-pair probe**: it knows exactly one pair of spellings that
|
|
755
|
+
# *may* indicate the contradiction (see the two `grep -qi` above). Its `✓` therefore
|
|
756
|
+
# means "that one pattern did not occur" — NOT "the User Stories are consistent". The
|
|
757
|
+
# property is semantic, and `/specpro-analyze` owns it (this is a declared stub; the
|
|
758
|
+
# comment at the top of the function says so and forbids extending it here).
|
|
759
|
+
# **An over-claim is caught by no later gate** — nothing compares a verdict's wording
|
|
760
|
+
# against what its test established (FR-038: a check whose result is decided by
|
|
761
|
+
# something outside its own intent reports a **value**, not a **finding**). ⇒ Stated,
|
|
762
|
+
# not left to be inferred from the source. ⚠️ **The first line is left as-is on
|
|
763
|
+
# purpose**: it is the checklist item's own name, and all twelve checks report
|
|
764
|
+
# "verdict mark + item name"; this line is what carries the honesty, not a rewording
|
|
765
|
+
# of the convention.
|
|
766
|
+
# ⚠️ **NO BACKTICKS around the command name** (`T248`/`ISS-223`): inside a double-quoted
|
|
767
|
+
# string a backtick pair is **command substitution** — bash executes `/specpro-analyze`
|
|
768
|
+
# (a path that does not exist), prints `No such file or directory` to stderr on every run,
|
|
769
|
+
# and substitutes the EMPTY result ⇒ this line printed `… — owns consistency`, i.e. it
|
|
770
|
+
# **erased the very name it exists to name**. A reader cannot tell a blanked name from one
|
|
771
|
+
# that was never written.
|
|
772
|
+
echo " scope: ONE keyword pair, not a semantic check — /specpro-analyze owns consistency"
|
|
773
|
+
PASSED=$((PASSED + 1))
|
|
774
|
+
fi
|
|
775
|
+
}
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
### check_cs002_fr_consistency()
|
|
779
|
+
|
|
780
|
+
```bash
|
|
781
|
+
check_cs002_fr_consistency() {
|
|
782
|
+
# Check for contradictory FRs
|
|
783
|
+
# Simplified: hardcoded keyword-pair check only — semantic consistency
|
|
784
|
+
# is owned by /specpro-analyze. Do not extend here.
|
|
785
|
+
local found=false
|
|
786
|
+
|
|
787
|
+
# Example: "store locally" vs "NOT use local storage"
|
|
788
|
+
if grep -qi "store.*data.*locally" "$FEATURE_SPEC" && grep -qi "NOT.*use.*local.*storage" "$FEATURE_SPEC"; then
|
|
789
|
+
found=true
|
|
790
|
+
fi
|
|
791
|
+
|
|
792
|
+
if [[ "$found" == "true" ]]; then
|
|
793
|
+
# ⚠️ **The verdict names what was MEASURED, not what it would like to conclude** (T170 /
|
|
794
|
+
# audit finding: "用一对固定短语的命中与否,断言一个全称结论"). The criterion probes ONE
|
|
795
|
+
# keyword pair; a `✓` that read "internally consistent" asserted a universal property
|
|
796
|
+
# nothing here checks. The `scope:` line below said so — the verdict line contradicted it.
|
|
797
|
+
echo " ✗ CS002: the keyword-pair probe MATCHED — possible contradiction"
|
|
798
|
+
echo " Possible contradiction detected"
|
|
799
|
+
|
|
800
|
+
# Manual intervention required
|
|
801
|
+
record_finding "CS002" "Possible contradiction in Functional Requirements"
|
|
802
|
+
FAILED=$((FAILED + 1))
|
|
803
|
+
FAILED_ITEMS+=("CS002")
|
|
804
|
+
else
|
|
805
|
+
echo " ✓ CS002: no contradiction detected by the keyword-pair probe"
|
|
806
|
+
# ⚠️ Same shape and same reason as CS001 above (T097 / FR-038): a declared stub —
|
|
807
|
+
# one hardcoded keyword pair — reporting under the item's semantic name.
|
|
808
|
+
# ⚠️ **NO BACKTICKS around the command name** (`T248`/`ISS-223`): inside a double-quoted
|
|
809
|
+
# string a backtick pair is **command substitution** — bash executes `/specpro-analyze`
|
|
810
|
+
# (a path that does not exist), prints `No such file or directory` to stderr on every run,
|
|
811
|
+
# and substitutes the EMPTY result ⇒ this line printed `… — owns consistency`, i.e. it
|
|
812
|
+
# **erased the very name it exists to name**. A reader cannot tell a blanked name from one
|
|
813
|
+
# that was never written.
|
|
814
|
+
echo " scope: ONE keyword pair, not a semantic check — /specpro-analyze owns consistency"
|
|
815
|
+
PASSED=$((PASSED + 1))
|
|
816
|
+
fi
|
|
817
|
+
}
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
### check_cs003_us_fr_mapping()
|
|
821
|
+
|
|
822
|
+
```bash
|
|
823
|
+
# ⚠️ **The empty-population guard, in ONE place** (T123 / FR-038 / 形态 6).
|
|
824
|
+
#
|
|
825
|
+
# The criteria below sample a POPULATION — the User Stories, the FR identifiers. When that
|
|
826
|
+
# population is empty the predicate is never exercised, and an **unfired predicate prints
|
|
827
|
+
# the same `✓` as a real pass**: the report cannot tell "checked and clean" from "never
|
|
828
|
+
# checked". `⊘` is the third value (T093) — shown distinctly, counted as neither pass nor
|
|
829
|
+
# fail. Measured (T123's replay): fed a spec consisting of one heading, seven criteria
|
|
830
|
+
# printed `✓` without deciding anything.
|
|
831
|
+
#
|
|
832
|
+
# ⚠️ **Not every empty-input pass is this defect.** A *search* criterion — "no forbidden
|
|
833
|
+
# pattern anywhere in the document" — returning nothing on an empty document is a **true
|
|
834
|
+
# negative**, and guarding it would be the mirror error. The guard belongs to the
|
|
835
|
+
# "**every X satisfies P**" shape, where an empty X makes the quantifier the whole answer.
|
|
836
|
+
# That is the shape `check_cp002` was guarded for in T093 and these two were not.
|
|
837
|
+
#
|
|
838
|
+
# usage: population_empty "<CHECK-ID>" "<count>" "<what was sampled>" && return
|
|
839
|
+
population_empty() {
|
|
840
|
+
local id=$1 count=$2 what=$3
|
|
841
|
+
if [[ ${count:-0} -eq 0 ]]; then
|
|
842
|
+
echo " ⊘ $id: skipped — 0 $what, so the criterion was never exercised"
|
|
843
|
+
echo " ⚠️ An unfired predicate is NOT a pass (FR-038)."
|
|
844
|
+
SKIPPED_ITEMS+=("$id")
|
|
845
|
+
SKIPPED=$((SKIPPED + 1))
|
|
846
|
+
return 0
|
|
847
|
+
fi
|
|
848
|
+
return 1
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
check_cs003_us_fr_mapping() {
|
|
852
|
+
# Check if all User Stories have associated FRs (NEW: supports grouped structure)
|
|
853
|
+
local us_without_fr=()
|
|
854
|
+
|
|
855
|
+
# Extract all US IDs
|
|
856
|
+
local us_ids=$(grep "^### US[0-9]" "$FEATURE_SPEC" | grep -oE "US[0-9]+" | sort -u)
|
|
857
|
+
|
|
858
|
+
# ⚠️ **0 User Stories ⇒ "every US has an FR" is vacuously true**: the quantifier has
|
|
859
|
+
# nothing to range over, so the check printed `✓` having decided nothing.
|
|
860
|
+
local us_count=$(printf '%s\n' "$us_ids" | grep -c . || true)
|
|
861
|
+
: "${us_count:=0}"
|
|
862
|
+
population_empty "CS003" "$us_count" "User Stories" && return
|
|
863
|
+
|
|
864
|
+
# The Requirements section, delimited by its heading and the next `## ` heading —
|
|
865
|
+
# NOT by a fixed line window. A hardcoded `-A 500` silently stops covering the
|
|
866
|
+
# section once a spec grows past it, and every US beyond the window is then
|
|
867
|
+
# reported as an orphan. (The section range must be expressed with awk, not a
|
|
868
|
+
# `sed` range: the start line matches the end pattern too, so the range would
|
|
869
|
+
# collapse to a single line — the same trap that made an auto-fix a no-op.)
|
|
870
|
+
local fr_section
|
|
871
|
+
fr_section=$(awk '
|
|
872
|
+
/^## Requirements/ { in_req = 1; print; next }
|
|
873
|
+
in_req && /^## / { exit }
|
|
874
|
+
in_req { print }
|
|
875
|
+
' "$FEATURE_SPEC")
|
|
876
|
+
|
|
877
|
+
for us in $us_ids; do
|
|
878
|
+
# NEW: Check if this US has FR group in Requirements section
|
|
879
|
+
# Look for pattern: "#### [Title] (US[ID])" in Requirements section.
|
|
880
|
+
#
|
|
881
|
+
# ⚠️ No `| head -1` on this pipeline: its exit status would be `head`'s, which
|
|
882
|
+
# is always 0, so `! 0` is never true and the check could not fail — a verdict
|
|
883
|
+
# that is always "pass" and is indistinguishable from a real pass. And a
|
|
884
|
+
# herestring rather than a pipe: grep exits at the first match, the writer
|
|
885
|
+
# takes SIGPIPE, and under `pipefail` the pipeline then reports 141 — a real
|
|
886
|
+
# orphan would read as an orphan only sometimes.
|
|
887
|
+
if ! grep -q "($us)" <<<"$fr_section"; then
|
|
888
|
+
us_without_fr+=("$us")
|
|
889
|
+
fi
|
|
890
|
+
done
|
|
891
|
+
|
|
892
|
+
if [[ ${#us_without_fr[@]} -gt 0 ]]; then
|
|
893
|
+
echo " ✗ CS003: US → FR mapping is complete"
|
|
894
|
+
echo " Orphan US: ${us_without_fr[*]}"
|
|
895
|
+
|
|
896
|
+
record_finding "CS003" "User Stories without FRs: ${us_without_fr[*]}"
|
|
897
|
+
FAILED=$((FAILED + 1))
|
|
898
|
+
FAILED_ITEMS+=("CS003")
|
|
899
|
+
else
|
|
900
|
+
echo " ✓ CS003: US → FR mapping is complete"
|
|
901
|
+
PASSED=$((PASSED + 1))
|
|
902
|
+
fi
|
|
903
|
+
}
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
### check_cs004_reference_integrity()
|
|
907
|
+
|
|
908
|
+
```bash
|
|
909
|
+
check_cs004_reference_integrity() {
|
|
910
|
+
# Reference integrity: every identifier mentioned in the spec must resolve to
|
|
911
|
+
# a definition in this document. Guards against dangling forward-reference
|
|
912
|
+
# namespaces (historical shape: several FRs carried a "(per SC-xxx)" reference
|
|
913
|
+
# while the spec had no Success Criteria section at all).
|
|
914
|
+
# a) every FR-nnn[A-E]? mention must match a defined FR entry
|
|
915
|
+
# b) every "(per X-nnn)" attribution must point at a defined FR
|
|
916
|
+
# ("(per Constitution)" style external references carry no digits: skipped)
|
|
917
|
+
# ⚠️ **`-o` on the FIRST grep, not only the second** (ISS-149 / T190). Without it the first
|
|
918
|
+
# grep selects the whole LINE, and the second harvests **every** FR id on that line — so a
|
|
919
|
+
# requirement that defines one FR and cites a DIFFERENT, undefined one put the cited id
|
|
920
|
+
# into `defined_frs`, and the dangling loop below then read the reference as resolved.
|
|
921
|
+
# (The measurement used concrete ids; they are not restated here — a value written into
|
|
922
|
+
# prose is indistinguishable from a current one to a mechanical reader, T202.) Measured:
|
|
923
|
+
# the same reference on a CONTINUATION line was caught (`FAILED=1`), on the definition line
|
|
924
|
+
# it was not (`FAILED=0`) — so the check was not always-true, it failed on one real way of
|
|
925
|
+
# writing it, which is harder to see. The trailing `:` narrows the yield to the definition
|
|
926
|
+
# form, and that form carries exactly one id.
|
|
927
|
+
local defined_frs=$(grep -oE '^[[:space:]]*- \*\*FR-[0-9]+[A-E]?\*\*:' "$FEATURE_SPEC" | grep -oE 'FR-[0-9]+[A-E]?' | sort -u)
|
|
928
|
+
local dangling=()
|
|
929
|
+
local ref
|
|
930
|
+
|
|
931
|
+
# ⚠️ **No FR identifier anywhere ⇒ "every reference resolves" is vacuously true** — there
|
|
932
|
+
# is nothing to range over and the `✓` reports a decision that was never made.
|
|
933
|
+
local ref_count=$(grep -oE 'FR-[0-9]+[A-E]?' "$FEATURE_SPEC" 2>/dev/null | sort -u | grep -c . || true)
|
|
934
|
+
: "${ref_count:=0}"
|
|
935
|
+
population_empty "CS004" "$ref_count" "FR identifiers (defined or referenced)" && return
|
|
936
|
+
|
|
937
|
+
# a) referenced FR IDs must be a subset of defined FR IDs
|
|
938
|
+
for ref in $(grep -oE 'FR-[0-9]+[A-E]?' "$FEATURE_SPEC" | sort -u); do
|
|
939
|
+
if ! grep -qx "$ref" <<< "$defined_frs"; then
|
|
940
|
+
dangling+=("$ref (referenced but not defined)")
|
|
941
|
+
fi
|
|
942
|
+
done
|
|
943
|
+
|
|
944
|
+
# b) "(per X-nnn)" attributions must point at a defined FR
|
|
945
|
+
for ref in $(grep -oE '\bper [A-Z]+-[0-9]+[A-E]?' "$FEATURE_SPEC" | sed 's/^per //' | sort -u); do
|
|
946
|
+
if ! grep -qx "$ref" <<< "$defined_frs"; then
|
|
947
|
+
dangling+=("$ref (per-reference to undefined identifier)")
|
|
948
|
+
fi
|
|
949
|
+
done
|
|
950
|
+
|
|
951
|
+
if [[ ${#dangling[@]} -gt 0 ]]; then
|
|
952
|
+
echo " ✗ CS004: Identifier references resolve to definitions"
|
|
953
|
+
echo " Dangling: ${dangling[*]}"
|
|
954
|
+
|
|
955
|
+
record_finding "CS004" "Dangling identifier references: ${dangling[*]}"
|
|
956
|
+
FAILED=$((FAILED + 1))
|
|
957
|
+
FAILED_ITEMS+=("CS004")
|
|
958
|
+
else
|
|
959
|
+
echo " ✓ CS004: Identifier references resolve to definitions"
|
|
960
|
+
PASSED=$((PASSED + 1))
|
|
961
|
+
fi
|
|
962
|
+
}
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
### check_tr001_constitution_constraints()
|
|
966
|
+
|
|
967
|
+
```bash
|
|
968
|
+
check_tr001_constitution_constraints() {
|
|
969
|
+
# Check if Constitution constraints are documented
|
|
970
|
+
local constitution="specs/constitution.md"
|
|
971
|
+
|
|
972
|
+
if [[ ! -f "$constitution" ]]; then
|
|
973
|
+
# ⚠️ This used to print "✓ … (no Constitution file)" and count as PASSED — a check
|
|
974
|
+
# that reached nothing, folded into a pass. With no constitution there is nothing to
|
|
975
|
+
# trace requirements to, so the predicate never runs. Third value, not a pass (T093).
|
|
976
|
+
echo " ⊘ TR001: skipped — no Constitution at $constitution"
|
|
977
|
+
SKIPPED_ITEMS+=("TR001")
|
|
978
|
+
echo " ⚠️ Nothing to trace constraints to. This is NOT a pass (the check did not run)."
|
|
979
|
+
SKIPPED=$((SKIPPED + 1))
|
|
980
|
+
return
|
|
981
|
+
fi
|
|
982
|
+
|
|
983
|
+
# Check if spec.md has Constitution Constraints section
|
|
984
|
+
if ! grep -q "^## Constitution Constraints" "$FEATURE_SPEC"; then
|
|
985
|
+
echo " ✗ TR001: Constitution constraints documented"
|
|
986
|
+
echo " Missing: Constitution Constraints section"
|
|
987
|
+
|
|
988
|
+
record_finding "TR001" "Constitution exists but constraints not documented in spec"
|
|
989
|
+
FAILED=$((FAILED + 1))
|
|
990
|
+
FAILED_ITEMS+=("TR001")
|
|
991
|
+
else
|
|
992
|
+
echo " ✓ TR001: Constitution constraints documented"
|
|
993
|
+
PASSED=$((PASSED + 1))
|
|
994
|
+
fi
|
|
995
|
+
}
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
### check_tr002_external_dependencies()
|
|
999
|
+
|
|
1000
|
+
```bash
|
|
1001
|
+
check_tr002_external_dependencies() {
|
|
1002
|
+
# ⚠️ **The source is the FUNCTIONAL layer, not the implementation stack** (T095/FR-040).
|
|
1003
|
+
# Names used to be derived from `plan.md` → `**Primary Dependencies**` — that is the
|
|
1004
|
+
# IMPLEMENTATION stack (libraries, frameworks, language versions). TR002 asks about the
|
|
1005
|
+
# functional layer's interoperation objects: the outside systems the *requirements* talk
|
|
1006
|
+
# to. Deriving from the wrong layer made the check's subject "which libraries do we link"
|
|
1007
|
+
# when its subject is "which outside systems does the product depend on".
|
|
1008
|
+
#
|
|
1009
|
+
# ⚠️ **And the judgment reads CONTENT, not the heading.** It used to be a single
|
|
1010
|
+
# `grep -qiE '^#+.*(external dependenc|dependencies|third.?party)' "$FEATURE_SPEC"` — it
|
|
1011
|
+
# passed as soon as a matching HEADING existed, reading nothing underneath it. An empty
|
|
1012
|
+
# `## External Dependencies` therefore satisfied a check whose entire point is that the
|
|
1013
|
+
# dependencies be *identified* (T095's 核验: a same-named EMPTY section MUST NOT pass).
|
|
1014
|
+
local derived_keys=()
|
|
1015
|
+
|
|
1016
|
+
# Outside systems named by the spec's own text — the functional layer.
|
|
1017
|
+
# ⚠️ **Restricted to NORMATIVE lines** (`MUST` / `SHALL` / `必须`). Without that filter a
|
|
1018
|
+
# sentence that *denies* external dependencies matches the bare noun and flips the verdict —
|
|
1019
|
+
# measured: `本项目自包含,无外部依赖。` derived `外部依赖` and then reported "external
|
|
1020
|
+
# systems named but no section". A requirement that an outside system be used is a
|
|
1021
|
+
# statement about the product; a passing mention (or a denial) is not.
|
|
1022
|
+
while IFS= read -r name; do
|
|
1023
|
+
[[ -n "$name" ]] && derived_keys+=("$name")
|
|
1024
|
+
# ⚠️ `外部` alone is too broad and `外部(系统|服务|依赖|接口|API)` is too narrow — measured:
|
|
1025
|
+
# `系统 MUST 对接外部支付网关 API` matched **neither** (the noun follows 网关, not 外部).
|
|
1026
|
+
# The middle ground is a bounded lookahead for a system-ish noun.
|
|
1027
|
+
done < <(grep -E 'MUST|SHALL|必须' "$FEATURE_SPEC" 2>/dev/null \
|
|
1028
|
+
| grep -vE '不(依赖|需要|涉及|对接)|无(外部|需)|MUST[[:space:]]+NOT|不需要' \
|
|
1029
|
+
| grep -oE '外部[^,。;、]{0,12}(系统|服务|依赖|接口|API|网关|数据库|平台)|第三方[^,。;、]*|external[[:space:]]+(system|service|API|dependenc)[^.,;]*|third.?party[^.,;]*' \
|
|
1030
|
+
| sed 's/[[:space:]]*$//' | sort -u || true)
|
|
1031
|
+
|
|
1032
|
+
# Read the section's BODY — the part the old judgment never looked at.
|
|
1033
|
+
# ⚠️ Both scans must be **case-insensitive**: headings are written `## External
|
|
1034
|
+
# Dependencies`, and a lowercase-only pattern finds nothing — which is how the first
|
|
1035
|
+
# version of this rewrite reported "no such section" for a spec that had one.
|
|
1036
|
+
local dep_section
|
|
1037
|
+
dep_section=$(awk '
|
|
1038
|
+
tolower($0) ~ /^#+.*(external dependenc|dependencies|third.?party)/ { insec=1; next }
|
|
1039
|
+
insec && /^#+ / { exit }
|
|
1040
|
+
insec { print }
|
|
1041
|
+
' "$FEATURE_SPEC")
|
|
1042
|
+
|
|
1043
|
+
if grep -qiE '^#+.*(external dependenc|dependencies|third.?party)' "$FEATURE_SPEC" 2>/dev/null; then
|
|
1044
|
+
# The section EXISTS. A heading with nothing under it is a defect in its own right —
|
|
1045
|
+
# either there is content to write or the heading should not be there.
|
|
1046
|
+
if [[ -z "${dep_section//[[:space:]]/}" ]]; then
|
|
1047
|
+
echo " ✗ TR002: the External Dependencies section exists but is EMPTY"
|
|
1048
|
+
echo " ⚠️ A heading alone is not an answer — the check reads the content, not the heading."
|
|
1049
|
+
record_finding "TR002" "External Dependencies section exists but is empty"
|
|
1050
|
+
FAILED=$((FAILED + 1))
|
|
1051
|
+
FAILED_ITEMS+=("TR002")
|
|
1052
|
+
return
|
|
1053
|
+
fi
|
|
1054
|
+
else
|
|
1055
|
+
# ⚠️ **FR-041 — before demanding the section, ask whether the TEMPLATE defines one.**
|
|
1056
|
+
# Measured (T096): `templates/spec-template.md` then defined no external-dependencies
|
|
1057
|
+
# section, so no spec generated from it could satisfy the branch below — the spec was
|
|
1058
|
+
# not at fault, the **criterion** was. This guard is the mechanism that reports it.
|
|
1059
|
+
# ⚠️ **Resolved by T189** (`ISS-147`, user ruling: option A): the template now carries
|
|
1060
|
+
# `## External Dependencies *(if applicable)*`, so this guard is **silent in this
|
|
1061
|
+
# project** and the three grounded outcomes below are reachable. It is kept — not dead
|
|
1062
|
+
# code — because it is what fires for *any* project whose template lacks the section,
|
|
1063
|
+
# and because it is what would have caught this one. ⚠️ **`*(if applicable)*` is
|
|
1064
|
+
# deliberately NOT `*(mandatory)*`**: `check_cp001` derives its required set from the
|
|
1065
|
+
# `*(mandatory)*` markers, so marking this one mandatory would silently turn the ruling
|
|
1066
|
+
# into "every spec must have an external-dependencies section".
|
|
1067
|
+
# ⚠️ **This guard comes FIRST**, ahead of the naming test: naming decides *how much to
|
|
1068
|
+
# say*, it does not decide whether the criterion is grounded.
|
|
1069
|
+
# ⚠️ **`-i` is load-bearing** — same trap as the section scan above, hit a second time
|
|
1070
|
+
# while writing this guard: the template spells it `## External Dependencies`, and a
|
|
1071
|
+
# lowercase-only pattern reports "the template defines no such section" for a template
|
|
1072
|
+
# that does. Caught by fixture ⑤ (grounded case must still FAIL) going to the ⊘ branch.
|
|
1073
|
+
if ! grep -qiE '^#+.*(external dependenc|dependencies|third.?party)' "$SPEC_TEMPLATE" 2>/dev/null; then
|
|
1074
|
+
echo " ⊘ TR002: not executable — the artifact template defines no such section"
|
|
1075
|
+
SKIPPED_ITEMS+=("TR002")
|
|
1076
|
+
echo " Checked: $SPEC_TEMPLATE"
|
|
1077
|
+
echo " ⚠️ This is a defect in the CHECK, not in the spec (FR-041): a spec whose own"
|
|
1078
|
+
echo " template offers no place to list external dependencies cannot be failed"
|
|
1079
|
+
echo " for not listing them. Fix the CRITERION, or give the template the section."
|
|
1080
|
+
if [[ ${#derived_keys[@]} -gt 0 ]]; then
|
|
1081
|
+
echo " Named in the spec: ${derived_keys[*]}"
|
|
1082
|
+
fi
|
|
1083
|
+
SKIPPED=$((SKIPPED + 1))
|
|
1084
|
+
return
|
|
1085
|
+
fi
|
|
1086
|
+
|
|
1087
|
+
# The template does define such a section, so the demand below is grounded. Only a
|
|
1088
|
+
# defect when the spec actually names an outside system.
|
|
1089
|
+
#
|
|
1090
|
+
# ⚠️ **This message used to read "no external system named in the spec" — a claim the
|
|
1091
|
+
# scan cannot support** (ISS-150 / T191). `derived_keys` recognises a dependency only
|
|
1092
|
+
# when it carries a LABEL — `外部`/`第三方`/`external …`/`third-party`. The functional-layer
|
|
1093
|
+
# names this criterion's own examples teach carry no such label:
|
|
1094
|
+
#
|
|
1095
|
+
# recognized=[external service to settle the order] ← the system MUST call the external service …
|
|
1096
|
+
# recognized=[] ← the system MUST call the payment gateway the checkout flow calls.
|
|
1097
|
+
#
|
|
1098
|
+
# The second line is verbatim from `templates/requirements-template.md` → TR002, i.e. the
|
|
1099
|
+
# form the criterion tells authors to write. So "not recognised" was being reported as
|
|
1100
|
+
# "not present", and the two are indistinguishable to the reader — 形态 6's neighbour: a
|
|
1101
|
+
# criterion silent on its own correct input.
|
|
1102
|
+
#
|
|
1103
|
+
# ⚠️ **The vocabulary was NOT widened to fix this, and that is the decision** (recorded in
|
|
1104
|
+
# `specs/research.md`): an unlabelled functional name has no distinguishing token — the
|
|
1105
|
+
# candidates (`store`, `service`, `provider`) are ordinary English words that appear in
|
|
1106
|
+
# unrelated requirements, so extending the list converts a false negative into a stream of
|
|
1107
|
+
# false positives. A false ✗ is not a safer error than a false ⊘; it is a louder one, and
|
|
1108
|
+
# the reader who hits it learns to ignore the check. What IS fixable here is the CLAIM.
|
|
1109
|
+
if [[ ${#derived_keys[@]} -eq 0 ]]; then
|
|
1110
|
+
echo " ⊘ TR002: not decidable by this scan — it recognises a dependency only when the"
|
|
1111
|
+
SKIPPED_ITEMS+=("TR002")
|
|
1112
|
+
echo " spec LABELS it (外部 / 第三方 / external … / third-party), and no such label"
|
|
1113
|
+
echo " was found. ⚠️ This is a limit of the CHECK, not a finding that the spec has no"
|
|
1114
|
+
echo " external dependencies — an unlabelled functional name (\"the payment gateway"
|
|
1115
|
+
echo " the checkout flow calls\", this criterion's own example) is not recognised."
|
|
1116
|
+
echo " Review the requirements manually for outside systems; record what you find in"
|
|
1117
|
+
echo " the spec's External Dependencies section."
|
|
1118
|
+
SKIPPED=$((SKIPPED + 1))
|
|
1119
|
+
return
|
|
1120
|
+
fi
|
|
1121
|
+
echo " ✗ TR002: external systems named but no External Dependencies section"
|
|
1122
|
+
echo " Named in the spec: ${derived_keys[*]}"
|
|
1123
|
+
record_finding "TR002" "External dependencies need documentation (named: ${derived_keys[*]})"
|
|
1124
|
+
FAILED=$((FAILED + 1))
|
|
1125
|
+
FAILED_ITEMS+=("TR002")
|
|
1126
|
+
return
|
|
1127
|
+
fi
|
|
1128
|
+
|
|
1129
|
+
# Section exists and has content. TR002's intent is that FAILURES were considered —
|
|
1130
|
+
# listing names is not enough (this is the half that made the check's own motivation
|
|
1131
|
+
# true rather than nominal).
|
|
1132
|
+
if ! printf '%s\n' "$dep_section" | grep -qiE 'fail|failure|timeout|mitigat|unavailable|degrad|fallback|error|超时|失败|不可用|降级|回退|异常|兜底'; then
|
|
1133
|
+
echo " ✗ TR002: dependencies listed but no FAILURE MODE stated"
|
|
1134
|
+
echo " TR002 asks whether external-dependency failures were CONSIDERED, not merely listed."
|
|
1135
|
+
record_finding "TR002" "External dependencies listed without failure modes"
|
|
1136
|
+
FAILED=$((FAILED + 1))
|
|
1137
|
+
FAILED_ITEMS+=("TR002")
|
|
1138
|
+
return
|
|
1139
|
+
fi
|
|
1140
|
+
|
|
1141
|
+
echo " ✓ TR002: External dependencies documented with failure modes"
|
|
1142
|
+
PASSED=$((PASSED + 1))
|
|
1143
|
+
}
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
---
|
|
1147
|
+
|
|
1148
|
+
## The repair path — REMOVED, and this is where it was ⚠️ [FR-058]
|
|
1149
|
+
|
|
1150
|
+
```bash
|
|
1151
|
+
# ⚠️ **Five functions used to live in this section. None of them exists any more.**
|
|
1152
|
+
# The list is kept — DESCRIBED rather than named — so the next reader does not
|
|
1153
|
+
# re-invent them without knowing they were tried:
|
|
1154
|
+
#
|
|
1155
|
+
# 1. the loop's "attempt to resolve" step — its own comment said it fixes nothing.
|
|
1156
|
+
# 2. the ONE write-back door — moved to the script named at the end of this note
|
|
1157
|
+
# (`T186` / `ISS-143`), and its ONLY remaining use is writing the checklist this
|
|
1158
|
+
# command itself produces.
|
|
1159
|
+
# 3. two prose rewriters — one edited `spec.md`'s FR lines, one reported on
|
|
1160
|
+
# performance-metric prose. Both wrote an artifact this command does not own.
|
|
1161
|
+
# 4. the authorisation guard that those two called first — the door that asked
|
|
1162
|
+
# "who authorized this repair?" (`ISS-163` / `T203`).
|
|
1163
|
+
#
|
|
1164
|
+
# ⚠️ **They are described rather than named on purpose.** The criteria these tasks are
|
|
1165
|
+
# verified by are greps for the old identifiers, and a note that spells them out would
|
|
1166
|
+
# make every one of those greps non-zero — so the check could never again tell "the
|
|
1167
|
+
# mechanism is gone" from "somebody wrote its name in a comment". The precedent is this
|
|
1168
|
+
# repository's own (`T172`: 散文让位于可复核性). **The full names and the reasoning are
|
|
1169
|
+
# in `specs/implement_issues.md` → `ISS-186` and in `specs/research.md` → `Addendum
|
|
1170
|
+
# (T220, 2026-09-19)`** — records, where a name belongs.
|
|
1171
|
+
#
|
|
1172
|
+
# **Why they are gone rather than "authorised"**: see the write-surface note at the top
|
|
1173
|
+
# of this file. Short version — **qc judges; it does not repair.** A repair performed by
|
|
1174
|
+
# the judge has spent the verdict (Principle III), and no caller-side parameter repairs
|
|
1175
|
+
# that. ⚠️ Two earlier rounds tried the parameter route; the 2026-09-19 ruling closed it.
|
|
1176
|
+
#
|
|
1177
|
+
# The script that remains: `scripts/bash/qc-auto-fix.sh` (+ its `.ps1` twin).
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
---
|
|
1181
|
+
|
|
1182
|
+
## register_findings() — route every non-passing check ⚠️ [FR-059]
|
|
1183
|
+
|
|
1184
|
+
```bash
|
|
1185
|
+
# ⚠️ **This is the step that turns "a check failed" into "something a named consumer
|
|
1186
|
+
# will read."** Without it a `✗` lives only in the checklist — and the checklist is
|
|
1187
|
+
# rebuilt on every run (FR-060), so that finding would survive exactly until the next
|
|
1188
|
+
# invocation. A finding that vanishes on the next run is indistinguishable from one
|
|
1189
|
+
# that was never reported (`FR-023`).
|
|
1190
|
+
#
|
|
1191
|
+
# ⚠️ **DEDUP IS NOT OPTIONAL.** This command runs on every spec creation and every spec
|
|
1192
|
+
# update (FR-062), so an undeduped writer appends the same failing check again and
|
|
1193
|
+
# again — and **a queue that has been flooded is no longer a queue.** The key is the
|
|
1194
|
+
# CHECK ID combined with the entry's OPEN state: an entry already resolved `[x]` MUST
|
|
1195
|
+
# NOT suppress a freshly-observed failure.
|
|
1196
|
+
register_findings() {
|
|
1197
|
+
local ledger="$IMPL_LEDGER"
|
|
1198
|
+
if [[ ! -f "$ledger" ]]; then
|
|
1199
|
+
echo " ⚠️ ledger not found: $ledger — findings stay in this report only" >&2
|
|
1200
|
+
return 0
|
|
1201
|
+
fi
|
|
1202
|
+
|
|
1203
|
+
local entry cid msg next_id
|
|
1204
|
+
for entry in "${FINDINGS[@]}"; do
|
|
1205
|
+
cid="${entry%%$'\t'*}"
|
|
1206
|
+
msg="${entry#*$'\t'}"
|
|
1207
|
+
|
|
1208
|
+
# (a) Dedup — is there already an OPEN entry for THIS check id?
|
|
1209
|
+
# ⚠️ Anchored on the entry body's line-start shape, never a full-text keyword
|
|
1210
|
+
# search: the id also appears in statistics rows and cross-references, and matching
|
|
1211
|
+
# one of those would silently SUPPRESS a real finding — the same false-skip the
|
|
1212
|
+
# ledger's own writer guards against.
|
|
1213
|
+
#
|
|
1214
|
+
# ⚠️ **The pattern is `\*\*${cid}\*\*` and NOT `\(\*\*${cid}\*\*\)`** — this is a
|
|
1215
|
+
# FIX, and the fixture is what caught it. The entry the awk below emits ends
|
|
1216
|
+
# `**CQ001**(source: …)` with a **full-width** parenthesis, while the first version
|
|
1217
|
+
# of this line looked for ASCII `(…)`. ⇒ **it could never match**, the dedup never
|
|
1218
|
+
# fired, and a second run appended the same finding again. ⚠️ **Measured, not
|
|
1219
|
+
# reasoned**: against the real emitted line, the old pattern returned **0**, this one
|
|
1220
|
+
# returns **1**, and `**CQ0010**` does not match `**CQ001**` (the `**` pair is the
|
|
1221
|
+
# boundary). A dedup guard that cannot fire is worse than no guard — it is a guard
|
|
1222
|
+
# somebody will trust.
|
|
1223
|
+
if grep -qE "^- \[ \] ISS-[0-9]+: .*\*\*${cid}\*\*" "$ledger"; then
|
|
1224
|
+
echo " ↺ ${cid}: already open in [specify] — not registered again"
|
|
1225
|
+
continue
|
|
1226
|
+
fi
|
|
1227
|
+
|
|
1228
|
+
# (b) Take the next id NUMERICALLY.
|
|
1229
|
+
# ⚠️ `sort` WITHOUT `-n` is wrong here and fails silently: ids are variable width,
|
|
1230
|
+
# so lexicographically ISS-100 sorts BEFORE ISS-97, and `sort | tail -1` would hand
|
|
1231
|
+
# back an id that is already in use. A collision is not self-announcing — the
|
|
1232
|
+
# statistics table still reconciles — while two entries sharing an id make the
|
|
1233
|
+
# address name neither of them.
|
|
1234
|
+
next_id=$(grep -oE '^- \[[x ]\] ISS-[0-9]+:' "$ledger" | grep -oE '[0-9]+' | sort -n | tail -1)
|
|
1235
|
+
next_id=$(( ${next_id:-0} + 1 ))
|
|
1236
|
+
|
|
1237
|
+
# (c) TWO writes in ONE pass: the entry, and the statistics row that summarises it.
|
|
1238
|
+
awk -v cid="$cid" -v msg="$msg" -v today="$(date '+%Y-%m-%d')" \
|
|
1239
|
+
-v newid="ISS-${next_id}" -v checklist="$REQUIREMENTS_CHECKLIST" '
|
|
1240
|
+
BEGIN { in_sec = 0; blanks = 0; done = 0 }
|
|
1241
|
+
function emit_body() {
|
|
1242
|
+
print "- [ ] " newid ": 质量检查未通过 —— **" cid "**(source: /specpro-qc " today ")"
|
|
1243
|
+
print " **Problem**: " msg
|
|
1244
|
+
print " **Recommendation**: `/specpro-specify --review-issues` 消费本条:该项由 `FR-061` 判为**不通过**;完整结论见 `" checklist "`。规格的修复归**规格的生产者**,不由检查方代行(`FR-058`)。"
|
|
1245
|
+
print " **Related**: **`FR-058`** – **`FR-062`**(qc 的职责与写入面)· `" checklist "`"
|
|
1246
|
+
done = 1
|
|
1247
|
+
}
|
|
1248
|
+
function flush_blanks() { while (blanks > 0) { print ""; blanks-- } }
|
|
1249
|
+
# (1) the statistics row — a DERIVED summary, in the same pass as the entry.
|
|
1250
|
+
# Written in a LATER step it is the known failure: the id enters the file before
|
|
1251
|
+
# the entry exists, so an idempotency guard greping for that id concludes
|
|
1252
|
+
# "already present" and skips the entry, while the summary still claims it landed.
|
|
1253
|
+
/^\| \[specify\] \|/ {
|
|
1254
|
+
n = split($0, f, "|"); total = f[3] + 1; pend = f[5] + 1
|
|
1255
|
+
items = f[6]; gsub(/^[ \t]+|[ \t]+$/, "", items)
|
|
1256
|
+
if (items == "") items = newid; else items = items " · " newid
|
|
1257
|
+
printf "| [specify] | %d |%s| %d | %s |\n", total, f[4], pend, items
|
|
1258
|
+
next
|
|
1259
|
+
}
|
|
1260
|
+
# (2) the entry, at the END of the [specify] section.
|
|
1261
|
+
# ⚠️ NOT the end of the file. The statistics block sits at the top so that a naive
|
|
1262
|
+
# append lands inside the LAST section; for any other section that misfiles the
|
|
1263
|
+
# entry where its consumer never reads it — and nothing errors, because the entry
|
|
1264
|
+
# still looks filed.
|
|
1265
|
+
/^## \[specify\] Phase Issues$/ { in_sec = 1; print; next }
|
|
1266
|
+
in_sec && (/^## \[/ || /^-->/) {
|
|
1267
|
+
if (!done) { print ""; emit_body() }
|
|
1268
|
+
flush_blanks(); in_sec = 0; print; next
|
|
1269
|
+
}
|
|
1270
|
+
in_sec && /^[ \t]*$/ { blanks++; next }
|
|
1271
|
+
in_sec { flush_blanks(); print; next }
|
|
1272
|
+
{ print }
|
|
1273
|
+
END { if (in_sec && !done) { print ""; emit_body() } flush_blanks() }
|
|
1274
|
+
' "$ledger" > "$ledger.tmp" && mv "$ledger.tmp" "$ledger" || {
|
|
1275
|
+
rm -f "$ledger.tmp"
|
|
1276
|
+
echo " ⚠️ ${cid}: could not write the entry — see the error above" >&2
|
|
1277
|
+
continue
|
|
1278
|
+
}
|
|
1279
|
+
REGISTERED_IDS+=("ISS-${next_id}")
|
|
1280
|
+
echo " → ${cid}: registered as ISS-${next_id} in [specify]"
|
|
1281
|
+
done
|
|
1282
|
+
|
|
1283
|
+
# ⚠️ Verify mechanically after writing — the ledger is a file FOUR other commands also
|
|
1284
|
+
# write into, so a structural break here surfaces as THEIR failure, later and elsewhere.
|
|
1285
|
+
if [[ ${#REGISTERED_IDS[@]} -gt 0 && -x .specpro/scripts/bash/verify-ledger.sh ]]; then
|
|
1286
|
+
if ! .specpro/scripts/bash/verify-ledger.sh >/dev/null 2>&1; then
|
|
1287
|
+
echo " ✗ ledger integrity FAILED after registering — run this and fix it:" >&2
|
|
1288
|
+
echo " .specpro/scripts/bash/verify-ledger.sh" >&2
|
|
1289
|
+
echo " The entries ARE written; what is wrong is the file's structure." >&2
|
|
1290
|
+
fi
|
|
1291
|
+
fi
|
|
1292
|
+
}
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
> **Fixture** (Constitution 2.2.1 — a criterion must carry a case set, including one that
|
|
1296
|
+
> **constructs an input making the criterion false**). Two cases, both observed:
|
|
1297
|
+
> - **Pass**: run the `awk` against a copy of the ledger → `verify-ledger.sh` reports
|
|
1298
|
+
> `✓ actual 32/30/2 = table` and all invariants hold.
|
|
1299
|
+
> - **Fail**: run it with the statistics stanza removed → `✗ actual 32/30/2 ≠ table
|
|
1300
|
+
> 31/30/1`. ⚠️ **This second case is the one that matters**: it is the observation that
|
|
1301
|
+
> the summary is not optional decoration — omitting it breaks the ledger.
|
|
1302
|
+
|
|
1303
|
+
---
|
|
1304
|
+
|
|
1305
|
+
## Helper Functions
|
|
1306
|
+
|
|
1307
|
+
### record_finding()
|
|
1308
|
+
|
|
1309
|
+
```bash
|
|
1310
|
+
# Collect ONE non-passing check into this run's finding set (FR-059).
|
|
1311
|
+
#
|
|
1312
|
+
# ⚠️ **This function writes NOTHING — and that is the change.** It used to append a line
|
|
1313
|
+
# to the clarification artifact, which belongs to `/specpro-specify` and holds
|
|
1314
|
+
# questions **the user** must answer. A criterion failure is not necessarily a user
|
|
1315
|
+
# question; it is a **finding for the spec's producer**. The write now happens once, in
|
|
1316
|
+
# `register_findings()`, which routes to the `[specify]` queue where a named consumer
|
|
1317
|
+
# reads it.
|
|
1318
|
+
#
|
|
1319
|
+
# ⚠️ **The pair (id, message) is kept, not just the id.** `FAILED_ITEMS` already carries
|
|
1320
|
+
# the ids, but a ledger entry needs prose; recording only the id would force the writer
|
|
1321
|
+
# to re-derive the sentence at write time, and re-derivation is where the specifics get
|
|
1322
|
+
# lost.
|
|
1323
|
+
record_finding() {
|
|
1324
|
+
local check_id=$1
|
|
1325
|
+
local issue=$2
|
|
1326
|
+
FINDINGS+=("${check_id}"$'\t'"${issue}")
|
|
1327
|
+
}
|
|
1328
|
+
```
|
|
1329
|
+
|
|
1330
|
+
### update_checklist()
|
|
1331
|
+
|
|
1332
|
+
```bash
|
|
1333
|
+
update_checklist() {
|
|
1334
|
+
# ⚠️ **REBUILD, not update — a ruling, not an optimisation** (FR-060, 2026-09-19).
|
|
1335
|
+
# The file is replaced wholesale from the template on EVERY run: no history across
|
|
1336
|
+
# runs, no cache, nothing carried over.
|
|
1337
|
+
#
|
|
1338
|
+
# ⚠️ **The rejected alternative was "build it once, then only re-check what failed."**
|
|
1339
|
+
# It leaks: `spec.md` changes, and re-running only the FAILED set never re-examines an
|
|
1340
|
+
# item that passed last time and has been broken since. This file's whole job is to be
|
|
1341
|
+
# the verdict on **this** spec.
|
|
1342
|
+
# ⚠️ It is also why nothing from a previous run may be kept here: **anything the
|
|
1343
|
+
# template cannot produce dies at this line.** Measured 2026-09-19 — a hand-accumulated
|
|
1344
|
+
# section in this file went from 10 rows to 0, and the file from 47086 to 13216 bytes,
|
|
1345
|
+
# **with no error anywhere**.
|
|
1346
|
+
cp "$REQUIREMENTS_TEMPLATE" "$REQUIREMENTS_CHECKLIST"
|
|
1347
|
+
|
|
1348
|
+
# Update with actual results — per-item `**Status**:` (T170).
|
|
1349
|
+
#
|
|
1350
|
+
# ⚠️ **This step used to be three comment lines saying it was omitted.** The audit of
|
|
1351
|
+
# 2026-09-18 found the consequence: three gates (`/specpro-implement` §1.5.b ·
|
|
1352
|
+
# `/specpro-tasks` §3.e · `/specpro-specify` §9.c) decide by reading this field, and
|
|
1353
|
+
# **nothing produced it** — the checklist kept the template's placeholder, every item read
|
|
1354
|
+
# as unresolved, and each of the three gates blocked. A consumer with no producer is not a
|
|
1355
|
+
# gate, it is a wall.
|
|
1356
|
+
#
|
|
1357
|
+
# ⚠️ **The id list comes from the CHECKLIST's own headings, never from a list here.** A
|
|
1358
|
+
# criterion added to the template must not be silently left blank, and a hand-written list
|
|
1359
|
+
# would do exactly that (the same "a list fails by omission" shape as the section lists).
|
|
1360
|
+
# ⚠️ An item that neither failed nor skipped is `x` — those are the only three outcomes a
|
|
1361
|
+
# check has (`PASSED` / `FAILED` / `SKIPPED` are incremented on every path).
|
|
1362
|
+
local tmp_status; tmp_status=$(mktemp)
|
|
1363
|
+
awk -v failed="${FAILED_ITEMS[*]:-}" -v skipped="${SKIPPED_ITEMS[*]:-}" '
|
|
1364
|
+
/^### / { id = $2; sub(/:$/, "", id) }
|
|
1365
|
+
# ⚠️ ONLY item headings. The template also contains a `- [ ] **Status**: …` line inside a
|
|
1366
|
+
# fenced EXAMPLE, and a rule written as "every Status line" rewrote that too — corrupting
|
|
1367
|
+
# the documentation inside the template itself. Measured: the first version of this writer
|
|
1368
|
+
# did exactly that. A check-id shape is the discriminator.
|
|
1369
|
+
# ⚠️ **No apostrophe anywhere in this awk body** (`T225`, found in passing): an `awk
|
|
1370
|
+
# ''...''` program sits inside a bash SINGLE-QUOTED string, so one apostrophe **closes the
|
|
1371
|
+
# string** and the rest of the block becomes a top-level syntax error. This line used to
|
|
1372
|
+
# read "the template''s own documentation" and did exactly that — the whole `update_checklist`
|
|
1373
|
+
# block could not be extracted and run as written. (`verify-doc-shell.sh` cannot catch it:
|
|
1374
|
+
# full shell syntax is outside its declared coverage.)
|
|
1375
|
+
/^- \[[ x]\] \*\*Status\*\*:/ && id ~ /^[A-Z]+[0-9]+$/ {
|
|
1376
|
+
if (index(" " failed " ", " " id " ") > 0) v = "✗ Failed"
|
|
1377
|
+
else if (index(" " skipped " ", " " id " ") > 0) v = "⊘ Skipped"
|
|
1378
|
+
else v = "x"
|
|
1379
|
+
print "- [x] **Status**: " v
|
|
1380
|
+
next
|
|
1381
|
+
}
|
|
1382
|
+
{ print }
|
|
1383
|
+
' "$REQUIREMENTS_CHECKLIST" > "$tmp_status"
|
|
1384
|
+
# ⚠️ The checkbox is set to `[x]` on EVERY item: completion is decided by the VALUE, not by
|
|
1385
|
+
# the box (the template says so itself) — and leaving the box unticked while the value says
|
|
1386
|
+
# `x` is the "two sources" shape this project keeps correcting.
|
|
1387
|
+
#
|
|
1388
|
+
# ⚠️ **The write goes through the door, and the door guards.** Its contract: a transformer
|
|
1389
|
+
# that exited non-zero, or produced an empty result, MUST NOT be allowed to replace the
|
|
1390
|
+
# destination. That guard IS the `ISS-111` / `T164` lesson — an earlier version wrote back
|
|
1391
|
+
# unconditionally and destroyed the target on a failed transform (measured then: a 48-byte
|
|
1392
|
+
# file became 0 bytes).
|
|
1393
|
+
# ⚠️ **The destination is passed AT THE CALL SITE** (T164). It used to be hardcoded inside
|
|
1394
|
+
# the door as `$FEATURE_SPEC`, so every run — the read-only default included — replaced
|
|
1395
|
+
# `spec.md` with the checklist. A hardcoded destination is an assumption the caller cannot
|
|
1396
|
+
# see, which is exactly why it went unnoticed for so long.
|
|
1397
|
+
scripts/bash/qc-auto-fix.sh apply-fix "$tmp_status" $? "$REQUIREMENTS_CHECKLIST" || return 1
|
|
1398
|
+
|
|
1399
|
+
# --- the summary block, in the SAME rebuild pass (`T225` / `ISS-193`) ---------------------
|
|
1400
|
+
#
|
|
1401
|
+
# ⚠️ **`T170` gave the per-item values a producer and left this block behind.** The summary
|
|
1402
|
+
# kept the template's placeholders — `**Status**: [PASS | BLOCK]` · `**Passed**: [X]` ·
|
|
1403
|
+
# `**Failed**: [Y]` — and `**Total Items**: 11` only looked right because the template
|
|
1404
|
+
# **spells that literal**. Consequences, both silent:
|
|
1405
|
+
# · **five readers across four commands** read a value that never existed
|
|
1406
|
+
# (`status` · `analyze` · `implement` · `plan` · `tasks`); each fell through to
|
|
1407
|
+
# "not BLOCK ⇒ proceed", so nothing stopped — it just never decided
|
|
1408
|
+
# · two of `verify-shapes.sh`'s three SHAPE-4 registrations were **permanently
|
|
1409
|
+
# `⊘ not executable`**, and a check that can never run is indistinguishable from one
|
|
1410
|
+
# that does not exist (shape 6)
|
|
1411
|
+
#
|
|
1412
|
+
# ⚠️ **Re-derive the numbers from the text just written — never from `$PASSED` / `$FAILED`.**
|
|
1413
|
+
# The counters are a second source for the same fact, and SHAPE-4 exists precisely because
|
|
1414
|
+
# "the report says N" must be comparable against "the artifact contains N". Reading both
|
|
1415
|
+
# from one text makes them agree **by construction**, which is the only version that cannot
|
|
1416
|
+
# drift. (The counters stay: they drive the exit code and the console summary.)
|
|
1417
|
+
#
|
|
1418
|
+
# ⚠️ **Two discriminators, and BOTH are load-bearing** — the first version of this writer
|
|
1419
|
+
# had only the first and produced `**Total Items**: 12` against a true count of 11:
|
|
1420
|
+
# 1. **the `- [ ]` prefix** — the template's *summary* line is `**Status**: [PASS | BLOCK]`
|
|
1421
|
+
# at line start, and it must not be counted as an item
|
|
1422
|
+
# 2. **fence state** — the template carries a fenced EXAMPLE whose `**Status**: …` line
|
|
1423
|
+
# starts with `- [ ]` and reads `⊘ Skipped`. Counting it inflates the total **and the
|
|
1424
|
+
# inflation is silent** — which is precisely the disagreement `verify-shapes.sh`'s
|
|
1425
|
+
# `SHAPE-4 checklist-total` exists to report. ⚠️ **The per-item writer above already
|
|
1426
|
+
# documented this same trap**; writing the second one without it reproduced the defect
|
|
1427
|
+
# the first one had been fixed for, in the same function.
|
|
1428
|
+
local tmp_summary; tmp_summary=$(mktemp)
|
|
1429
|
+
awk '
|
|
1430
|
+
# ⚠️ **The fence test is spelled by CONCATENATION — `"`" "`" "`"` — and that is not
|
|
1431
|
+
# stylistic**: a literal run of three backticks **inside this fenced block would close
|
|
1432
|
+
# the block**, truncating the snippet for every fence-aware reader (the embedded-shell
|
|
1433
|
+
# verifier extracts snippets by fence; it would then scan half a program and call the
|
|
1434
|
+
# result clean). Measured: the literal spelling was written first and did exactly that.
|
|
1435
|
+
{ t = $0; sub(/^[[:space:]]+/, "", t) }
|
|
1436
|
+
substr(t, 1, 3) == "`" "`" "`" { infence = !infence; lines[NR] = $0; next }
|
|
1437
|
+
/^- \[[ x]\] \*\*Status\*\*:/ && !infence {
|
|
1438
|
+
total++
|
|
1439
|
+
if ($0 ~ /\*\*Status\*\*:[[:space:]]*x[[:space:]]*$/) passed++
|
|
1440
|
+
else if ($0 ~ /\*\*Status\*\*:[[:space:]]*✗/) failed++
|
|
1441
|
+
lines[NR] = $0
|
|
1442
|
+
next
|
|
1443
|
+
}
|
|
1444
|
+
{ lines[NR] = $0 }
|
|
1445
|
+
END {
|
|
1446
|
+
# FR-061: pass ⟺ no ✗. `⊘ Skipped` is neither — it must not tip this either way, so it
|
|
1447
|
+
# is counted as neither passed nor failed. ⚠️ `+ 0` is not cosmetic: an unset awk
|
|
1448
|
+
# variable concatenates as the EMPTY STRING, so `**Failed**: ` would render blank
|
|
1449
|
+
# instead of `0`.
|
|
1450
|
+
status = (failed == 0) ? "PASS" : "BLOCK"
|
|
1451
|
+
for (i = 1; i <= NR; i++) {
|
|
1452
|
+
l = lines[i]
|
|
1453
|
+
if (l ~ /^\*\*Status\*\*: /) print "**Status**: " status
|
|
1454
|
+
else if (l ~ /^\*\*Total Items\*\*: /) print "**Total Items**: " (total + 0)
|
|
1455
|
+
else if (l ~ /^\*\*Passed\*\*: /) print "**Passed**: " (passed + 0)
|
|
1456
|
+
else if (l ~ /^\*\*Failed\*\*: /) print "**Failed**: " (failed + 0)
|
|
1457
|
+
else print l
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
' "$REQUIREMENTS_CHECKLIST" > "$tmp_summary"
|
|
1461
|
+
scripts/bash/qc-auto-fix.sh apply-fix "$tmp_summary" $? "$REQUIREMENTS_CHECKLIST" || return 1
|
|
1462
|
+
# ⚠️ **There is no "QC history row" step any more.** It appended a row per iteration
|
|
1463
|
+
# before the file's end-of-file sentinel — and with the loop gone (FR-060) there are no
|
|
1464
|
+
# iterations to record, and with the file rebuilt every run there is nothing for a
|
|
1465
|
+
# history to accumulate into. The sentinel itself STAYS: it is a boundary marker, and
|
|
1466
|
+
# `plan.md` → Constraint 7 shape 8 still cites it as a working example of one.
|
|
1467
|
+
}
|
|
1468
|
+
```
|
|
1469
|
+
|
|
1470
|
+
## Notes
|
|
1471
|
+
|
|
1472
|
+
- **Called automatically by `/specpro-specify`** — on the spec's first creation and on
|
|
1473
|
+
**every** update (`FR-062`). The trigger belongs to the spec's change, not to the
|
|
1474
|
+
caller's memory. It may also be run directly to see the current verdict.
|
|
1475
|
+
- **It reports; it does not repair** (`FR-058`). Every non-passing criterion becomes an
|
|
1476
|
+
entry in the ledger's `[specify]` queue (`FR-059`), whose consumer is
|
|
1477
|
+
`/specpro-specify --review-issues` — **that** is where the spec gets fixed.
|
|
1478
|
+
- **The checklist is rebuilt from the template on every run** (`FR-060`): there is no
|
|
1479
|
+
history across runs, no cache, and nothing carried over. ⇒ Do **not** hand-accumulate
|
|
1480
|
+
content in `specs/checklists/requirements.md`; the next run will delete it silently.
|
|
1481
|
+
Cross-run records belong in an artifact that is not rebuilt — see
|
|
1482
|
+
`specs/audit/known-tool-defects.md` for the worked case.
|
|
1483
|
+
- **The verdict is the per-item values** (`FR-061`): `x` = passed, `⊘ Skipped` = the
|
|
1484
|
+
criterion does not apply here, `✗ Failed` = did not pass. **Passing means "no `✗`"** —
|
|
1485
|
+
a skip is neither a pass nor a failure and MUST NOT be folded into either.
|
|
1486
|
+
- **This command does not decide feasibility.** It used to end by declaring a project
|
|
1487
|
+
"not feasible" when it could not repair its way to a pass — a conclusion it had no
|
|
1488
|
+
standing to reach, since it never had the repair power to test it. It now stops at
|
|
1489
|
+
the verdict, and the queue carries the rest.
|