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.
Files changed (76) hide show
  1. specpro_cli/__init__.py +16 -0
  2. specpro_cli/assets/commands/specpro.analyze.md +1102 -0
  3. specpro_cli/assets/commands/specpro.checklist.md +335 -0
  4. specpro_cli/assets/commands/specpro.clarify.md +581 -0
  5. specpro_cli/assets/commands/specpro.constitution.md +488 -0
  6. specpro_cli/assets/commands/specpro.feature.md +115 -0
  7. specpro_cli/assets/commands/specpro.implement.md +1881 -0
  8. specpro_cli/assets/commands/specpro.manual-test.md +206 -0
  9. specpro_cli/assets/commands/specpro.plan.md +3284 -0
  10. specpro_cli/assets/commands/specpro.qc.md +1489 -0
  11. specpro_cli/assets/commands/specpro.scenarios.md +154 -0
  12. specpro_cli/assets/commands/specpro.specify.md +1449 -0
  13. specpro_cli/assets/commands/specpro.status.md +863 -0
  14. specpro_cli/assets/commands/specpro.tasks.md +1207 -0
  15. specpro_cli/assets/commands/specpro.test-implement.md +462 -0
  16. specpro_cli/assets/commands/specpro.test-plan.md +383 -0
  17. specpro_cli/assets/commands/specpro.user-manual.md +178 -0
  18. specpro_cli/assets/scripts/bash/check-anti-coupling.sh +293 -0
  19. specpro_cli/assets/scripts/bash/check-prerequisites.sh +176 -0
  20. specpro_cli/assets/scripts/bash/common.sh +88 -0
  21. specpro_cli/assets/scripts/bash/create-new-feature.sh +336 -0
  22. specpro_cli/assets/scripts/bash/qc-auto-fix.sh +121 -0
  23. specpro_cli/assets/scripts/bash/setup-plan.sh +60 -0
  24. specpro_cli/assets/scripts/bash/verify-cumulative-records.sh +203 -0
  25. specpro_cli/assets/scripts/bash/verify-deliverables-tracked.sh +147 -0
  26. specpro_cli/assets/scripts/bash/verify-deployment.sh +239 -0
  27. specpro_cli/assets/scripts/bash/verify-frontmatter-yaml.sh +63 -0
  28. specpro_cli/assets/scripts/bash/verify-ledger.sh +376 -0
  29. specpro_cli/assets/scripts/bash/verify-shapes.sh +1082 -0
  30. specpro_cli/assets/scripts/git-hooks/pre-commit +243 -0
  31. specpro_cli/assets/scripts/install-git-hooks.sh +67 -0
  32. specpro_cli/assets/scripts/powershell/check-anti-coupling.ps1 +249 -0
  33. specpro_cli/assets/scripts/powershell/check-prerequisites.ps1 +148 -0
  34. specpro_cli/assets/scripts/powershell/common.ps1 +95 -0
  35. specpro_cli/assets/scripts/powershell/create-new-feature.ps1 +229 -0
  36. specpro_cli/assets/scripts/powershell/qc-auto-fix.ps1 +110 -0
  37. specpro_cli/assets/scripts/powershell/setup-plan.ps1 +61 -0
  38. specpro_cli/assets/scripts/powershell/verify-cumulative-records.ps1 +133 -0
  39. specpro_cli/assets/scripts/powershell/verify-deliverables-tracked.ps1 +112 -0
  40. specpro_cli/assets/scripts/powershell/verify-deployment.ps1 +278 -0
  41. specpro_cli/assets/scripts/powershell/verify-frontmatter-yaml.ps1 +56 -0
  42. specpro_cli/assets/scripts/powershell/verify-ledger.ps1 +383 -0
  43. specpro_cli/assets/scripts/powershell/verify-shapes.ps1 +978 -0
  44. specpro_cli/assets/templates/agent-context-template.md +49 -0
  45. specpro_cli/assets/templates/assumptions-template.md +248 -0
  46. specpro_cli/assets/templates/checklist-template.md +40 -0
  47. specpro_cli/assets/templates/clarifications-template.md +155 -0
  48. specpro_cli/assets/templates/constitution-template.md +50 -0
  49. specpro_cli/assets/templates/feature-spec-template.md +66 -0
  50. specpro_cli/assets/templates/plan-overview-template.md +150 -0
  51. specpro_cli/assets/templates/plan-template.md +387 -0
  52. specpro_cli/assets/templates/protocol-golden-bytes-guide.md +195 -0
  53. specpro_cli/assets/templates/requirements-template.md +356 -0
  54. specpro_cli/assets/templates/spec-template.md +267 -0
  55. specpro_cli/assets/templates/tasks-template.md +252 -0
  56. specpro_cli/assets/templates/test-tasks-template.md +174 -0
  57. specpro_cli/cli/__init__.py +5 -0
  58. specpro_cli/cli/cmd_init.py +416 -0
  59. specpro_cli/cli/cmd_remove.py +122 -0
  60. specpro_cli/cli/entry.py +181 -0
  61. specpro_cli/integrations/__init__.py +36 -0
  62. specpro_cli/integrations/base.py +601 -0
  63. specpro_cli/integrations/claude/__init__.py +101 -0
  64. specpro_cli/integrations/copilot/__init__.py +153 -0
  65. specpro_cli/integrations/cursor_agent/__init__.py +51 -0
  66. specpro_cli/integrations/gemini/__init__.py +44 -0
  67. specpro_cli/integrations/opencode/__init__.py +48 -0
  68. specpro_cli/integrations/qodercli/__init__.py +54 -0
  69. specpro_cli/integrations/registry.py +88 -0
  70. specpro_cli/packaged/__init__.py +5 -0
  71. specpro_cli/packaged/sync.py +106 -0
  72. specpro_cli-0.1.0.dist-info/METADATA +117 -0
  73. specpro_cli-0.1.0.dist-info/RECORD +76 -0
  74. specpro_cli-0.1.0.dist-info/WHEEL +4 -0
  75. specpro_cli-0.1.0.dist-info/entry_points.txt +2 -0
  76. 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.