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,1082 @@
1
+ #!/bin/bash
2
+ #
3
+ # Shape-detection verifier — one re-runnable detection per registered defect shape.
4
+ #
5
+ # Why this exists: `spec.md` -> FR-048 要求登记表中**每一个**形态 MUST 有一条**可重跑**
6
+ # 的检出方式,且**消除**以重跑检出成立、**不以"已修"的声明**(US10 的 Acceptance
7
+ # Scenario 5)。`plan.md` -> Constraint 7 定了形态登记表,其「落点」列已按同处的
8
+ # 「落点政策」重新解释:可执行的实现体落 `scripts/`,命令文档只**调用**它们。
9
+ # This script is that implementation.
10
+ #
11
+ # ⚠️ THE RULE THIS FILE OBEYS, AND THE ONE IT IS MOST LIKELY TO BREAK:
12
+ # **一条恒真的检出与一条不存在的检出,在报告上同形**(形态 6)。 A check over an
13
+ # empty registry prints nothing, and "nothing" reads exactly like "nothing wrong".
14
+ # ⇒ **Every check here declares its own scope, and an empty scope is `⊘`, never `✓`.**
15
+ # The same guard the QC criterion gained in T096/FR-041, applied to this file's checks.
16
+ #
17
+ # ⚠️ A second consequence, stated rather than papered over: several of the eleven shapes
18
+ # cannot be decided by a generic rule over the whole repository — shape 2 needs to know
19
+ # WHICH four wiring points a given rule claims; shape 3 needs to be told WHICH fact
20
+ # appears in two places. Those checks therefore run against an **explicit registry**,
21
+ # declared below. A registry that is thin is visible as such; a check that guessed its
22
+ # subjects would instead fire on innocent files, and a check that fires on innocent files
23
+ # is turned off by its readers — after which it is a decoration.
24
+ #
25
+ # ⚠️ What this verifier does NOT do: it has no enforcement point. Per 落点政策 §4 it is
26
+ # the CALLER that creates one, and today no caller runs it (`ISS-90` ③b records that gap
27
+ # for the fixture cases as well). This file reports; something else must stop on it.
28
+ #
29
+ # Usage: scripts/bash/verify-shapes.sh
30
+ # SHAPES_HOOK_BASE=<path> scripts/bash/verify-shapes.sh (verify a copy)
31
+ # Exit: 0 = no shape has a hit; 1 = at least one hit (details printed)
32
+
33
+ # ⚠️ NOT `set -e`: the logic below reads the exit codes of subcommands, and `set -e`
34
+ # would end the run before the reading happens (宪法 2.2.2, second carve-out).
35
+ set -uo pipefail
36
+
37
+ # Resolved from THIS script's location, not the caller's cwd — the fixture cases run it
38
+ # from a temporary tree, and a cwd-relative base would then verify the wrong place.
39
+ BASE="${SHAPES_HOOK_BASE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
40
+
41
+ HITS=0
42
+ OKS=0
43
+ SKIPS=0
44
+
45
+ hit() { HITS=$((HITS + 1)); echo " ✗ $1"; }
46
+ ok() { OKS=$((OKS + 1)); echo " ✓ $1"; }
47
+ skip() { SKIPS=$((SKIPS + 1)); echo " ⊘ $1"; }
48
+
49
+ # ---------------------------------------------------------------------------------------
50
+ # ⊘ sites that are a LIVE SILENCE — 形态 12's subject, collected as DATA
51
+ # ---------------------------------------------------------------------------------------
52
+ # `skip` above means "this subject could not be exercised **here**" (absent carrier, stale
53
+ # registry row, empty registry). `unreceived` means something stronger: **the subject
54
+ # exists and was not judged**, so somebody must own filling its scope.
55
+ #
56
+ # ⚠️ Why the receiver is a COLLECTED PAIR and not parsed back out of the report text
57
+ # (`T213` / `ISS-177`): deriving the subject from the message is 形态 1 — a semantic
58
+ # conclusion drawn from a literal match — and it would go silently dead the first time
59
+ # somebody reworded a message. The messages below are NOT uniform (some carry a subject id,
60
+ # some do not), so there is nothing stable to parse even today.
61
+ UNRECEIVED=""
62
+ unreceived() { # unreceived <receiver-id-or-empty> <message>
63
+ SKIPS=$((SKIPS + 1))
64
+ UNRECEIVED="${UNRECEIVED}${1}"$'\t'"${2}"$'\n'
65
+ echo " ⊘ $2"
66
+ }
67
+
68
+ # Is <id> present as an ENTRY — not as a mention? The pattern is the entry body's
69
+ # line-start shape, which is the discipline the ledger's own verifier already uses
70
+ # (`^- \[[x ]\] ISS-<N>:` rather than a full-text ID search; `TOOL-009`). A full-text
71
+ # match would count every cross-reference as a receiver, and this file's own commentary
72
+ # names these ids — so the weaker check would pass on its own prose.
73
+ receivers_exist() { # receivers_exist <id>
74
+ local id=$1 f
75
+ for f in "specs/tasks.md" "specs/implement_issues.md"; do
76
+ [ -f "$BASE/$f" ] || continue
77
+ grep -qE "^- \[[x ]\] ${id}([ :]|\$)" "$BASE/$f" && return 0
78
+ done
79
+ return 1
80
+ }
81
+
82
+ # Count of lines matching a regex in a file, as a number. `grep -c` prints 0 AND exits 1
83
+ # on no match, so `$(grep -c … || echo 0)` yields "0\n0" — a two-line string that breaks
84
+ # every numeric comparison it is fed to (the `|| echo "0"` trap, fixed repo-wide in T125).
85
+ count_of() { # count_of <file> <regex>
86
+ local n
87
+ n=$(grep -cE "$2" "$1" 2>/dev/null || true)
88
+ echo "${n:-0}"
89
+ }
90
+
91
+ count_of_unfenced() { # count_of_unfenced <file> <regex> — 只数围栏**之外**的行
92
+ # ⚠️ **为什么必须有这一支**(`T222` 同轮实测):`specs/checklists/requirements.md` 由模板
93
+ # 重建,而**模板里有一条 `- [ ] **Status**: ⊘ Skipped` 的 fenced EXAMPLE** —— 它示范的是
94
+ # 取值语义。逐行计数的判据会把它算成**第 12 项**,而清单里只有 **11** 个 `### ID:` 条目
95
+ # ⇒ 报出一个**假 hit**。同一个判别器 `update_checklist` 的 awk **已经用了**
96
+ # (`id ~ /^[A-Z]+[0-9]+$/`),只是这一处没有。**围栏就是那条示例的判别器**:真条目不在围栏里。
97
+ # ⚠️ **The regex travels through ENVIRON, NOT through `-v`** — and that is a FIX, not
98
+ # a style choice. `awk -v re='...\[...'` makes awk process the argument's backslash
99
+ # ESCAPES, so `\[` arrives as a bare `[` and `\*` as a bare `*` (a quantifier): the
100
+ # pattern silently stops matching and the count comes back **0** — a wrong number with
101
+ # no error, measured on this very call (`n=0` while `grep -cE` said 12). `ENVIRON` is
102
+ # read verbatim, escapes intact.
103
+ RE="$2" awk '
104
+ /^[[:space:]]*```/ { infence = !infence; next }
105
+ !infence && $0 ~ ENVIRON["RE"] { n++ }
106
+ END { print n + 0 }
107
+ ' "$1" 2>/dev/null
108
+ }
109
+
110
+ echo "Shape detections: $BASE"
111
+ echo
112
+
113
+ # =======================================================================================
114
+ # SHAPE 1 — literal match used as a semantic conclusion
115
+ # =======================================================================================
116
+ # 判据: 误报即命中. The probe is a COMPLIANT text in which every listed keyword appears
117
+ # only as a SUBSTRING of an unrelated word (`class` inside `classify`, `def` inside
118
+ # `definition`). A keyword scan that matches those is drawing a semantic conclusion from
119
+ # a literal match. ⚠️ The probe deliberately does NOT contain whole-word uses such as
120
+ # "the interface between two layers": that case still matches by design, it is documented
121
+ # as a declared limit of a keyword scan, and testing a declared non-property would be a
122
+ # false positive of this detector rather than of the criterion (形态 1, turned around).
123
+ #
124
+ # ⚠️ Why the criterion is EXTRACTED from its carrier rather than re-stated here: a second
125
+ # copy of the predicate is a second source (形态 3) and would drift silently. The adapter
126
+ # below is the only way to reach a criterion that is still carried in Markdown — which is
127
+ # precisely the state 落点政策 §1 exists to end (`T186` carries that migration).
128
+ echo " [1] literal match used as a semantic conclusion"
129
+
130
+ # --- the criterion harness, shared by shapes 1 and 6 -----------------------------------
131
+ #
132
+ # ⚠️ Why the criterion is EXTRACTED from its carrier rather than re-stated here: a second
133
+ # copy of the predicate is a second source (形态 3) and would drift silently. Extraction is
134
+ # the only way to reach a criterion that is still carried in Markdown — which is precisely
135
+ # the state 落点政策 §1 exists to end (`T186` carries that migration for one family).
136
+ #
137
+ # ⚠️ The harness FAILS LOUDLY when extraction stops working. An extraction that silently
138
+ # returns nothing is ISS-83's shape: the criterion never runs and the run reads as clean.
139
+ extract_fence() { # extract_fence <carrier> <fn-name> — the fence that DEFINES <fn-name>
140
+ local carrier=$1 fn=$2
141
+ [ -f "$carrier" ] || return 0
142
+ awk -v fn="$fn" '
143
+ /^```bash/ { infence=1; buf=""; next }
144
+ /^```/ { if (infence && buf ~ (fn "\\(\\)")) { printf "%s", buf; exit } infence=0; next }
145
+ infence { buf = buf $0 "\n" }
146
+ ' "$carrier"
147
+ }
148
+
149
+ # The probes. Two per criterion: one it must NOT report on, one it MUST.
150
+ crit_probe() { # crit_probe <criterion-id> <compliant|falsifying>
151
+ case "$1-$2" in
152
+ CQ001-compliant) cat <<'P'
153
+ ## Requirements *(mandatory)*
154
+
155
+ ### Functional Requirements
156
+
157
+ - **FR-001**: The system MUST classify incoming events into categories.
158
+ **Lifecycle**: [specify:create][plan:create][tasks:create]
159
+ System MUST classify each event and record its category.
160
+
161
+ - **FR-002**: The system MUST show a definition of every category it reports.
162
+ **Lifecycle**: [specify:create][plan:create][tasks:create]
163
+ System MUST present a declarative summary for each category.
164
+ P
165
+ ;;
166
+ CQ001-falsifying) cat <<'P'
167
+ ## Requirements *(mandatory)*
168
+
169
+ ### Functional Requirements
170
+
171
+ - **FR-001**: The system MUST render its configuration screen with React.
172
+ **Lifecycle**: [specify:create][plan:create][tasks:create]
173
+ System MUST use an async/await HTTP client for the sync endpoint.
174
+ P
175
+ ;;
176
+ CS003-compliant) cat <<'P'
177
+ ### US1 - Alpha (Priority: P1)
178
+
179
+ **Independent Test**: run it.
180
+
181
+ ### US2 - Beta (Priority: P2)
182
+
183
+ **Independent Test**: run it.
184
+
185
+ ## Requirements *(mandatory)*
186
+
187
+ #### Alpha (US1)
188
+
189
+ - **FR-001**: The system MUST do alpha.
190
+
191
+ #### Beta (US2)
192
+
193
+ - **FR-002**: The system MUST do beta.
194
+ P
195
+ ;;
196
+ CS003-falsifying) cat <<'P'
197
+ ### US1 - Alpha (Priority: P1)
198
+
199
+ **Independent Test**: run it.
200
+
201
+ ### US2 - Beta (Priority: P2)
202
+
203
+ **Independent Test**: run it.
204
+
205
+ ## Requirements *(mandatory)*
206
+
207
+ #### Alpha (US1)
208
+
209
+ - **FR-001**: The system MUST do alpha.
210
+ P
211
+ ;;
212
+ # ---- the rest (T192, minus one retired 2026-09-19 / T197) ----------------------
213
+ # Each is a MINIMAL input that falsifies its criterion's predicate. They are the
214
+ # cases T124 ran through a throwaway harness; without a home here they had no
215
+ # owner at all (ISS-151) — the obligation had been written onto a task that could
216
+ # not write this file. ⚠️ The probe for the retired criterion was removed with it:
217
+ # a probe for a criterion that no longer exists would be registered against a
218
+ # `CRITERIA` row that no longer exists, and `S6_DECLARED` would then never reach it.
219
+ CQ003-falsifying) cat <<'P'
220
+ ## Requirements
221
+
222
+ - **FR-001**: The system MUST implement search using React for the index.
223
+ P
224
+ ;;
225
+ CP001-falsifying) cat <<'P'
226
+ ## Requirements *(mandatory)*
227
+
228
+ ### Functional Requirements
229
+
230
+ - **FR-001**: The system MUST do the thing.
231
+ P
232
+ ;;
233
+ # ⚠️ US1 carries no `**Acceptance Scenarios**` block at all; US2 does. The criterion
234
+ # compares the two counts, so a spec where BOTH were missing would still be caught —
235
+ # but a spec with ONE missing is the smaller failing input, and this is it.
236
+ CP002-falsifying) cat <<'P'
237
+ ### US1 - Alpha (Priority: P1)
238
+
239
+ No scenarios here at all.
240
+
241
+ ### US2 - Beta (Priority: P2)
242
+
243
+ **Acceptance Scenarios**:
244
+
245
+ 1. **Given** a, **When** b, **Then** c
246
+ P
247
+ ;;
248
+ # The metric sits INSIDE an Acceptance Scenarios block: CP003 does not scan the whole
249
+ # document, it scans that block — a metric anywhere else would falsify nothing here.
250
+ CP003-falsifying) cat <<'P'
251
+ ### US1 - Alpha (Priority: P1)
252
+
253
+ **Acceptance Scenarios**:
254
+
255
+ 1. **Given** a live session, **When** the screen changes, **Then** it updates within 200ms
256
+
257
+ **Why this priority**: because.
258
+ P
259
+ ;;
260
+ CS001-falsifying) cat <<'P'
261
+ ### US1 - Alpha (Priority: P1)
262
+
263
+ The system MUST support offline mode for disconnected sites.
264
+
265
+ ### US2 - Beta (Priority: P2)
266
+
267
+ The system MUST require an always-on connection to the server.
268
+ P
269
+ ;;
270
+ CS002-falsifying) cat <<'P'
271
+ ## Requirements
272
+
273
+ - **FR-001**: The system MUST store the data locally on the device.
274
+ - **FR-002**: The system MUST NOT use local storage for records.
275
+ P
276
+ ;;
277
+ # ⚠️ The reference is on the DEFINITION line, and that is the point: it is the form
278
+ # T190 fixed. A continuation-line variant was already caught before that fix, so a
279
+ # probe written that way would report green against the broken criterion too — it
280
+ # would test nothing about the repair.
281
+ #
282
+ # ⚠️ **Why the cited id is `FR-042` and not a number outside the project's own range**
283
+ # this probe's subject is "**cited inside this sample but not defined in it**" — the
284
+ # id only has to be absent *from the sample*, and any number satisfies that. A number
285
+ # ABOVE the project's own range, however, is indistinguishable from a **foreign
286
+ # identifier** to `check-anti-coupling.sh`'s class 1, so the fixture itself became an
287
+ # O1 finding and kept that class's hit count permanently non-zero. Using an id inside
288
+ # the project's range removes the false finding **without touching the probe's
289
+ # subject or the criterion**.
290
+ # ⚠️ When writing a new probe, pick an id the project allocates but this sample does
291
+ # not define — never one the project has never heard of.
292
+ CS004-falsifying) cat <<'P'
293
+ ## Requirements
294
+
295
+ - **FR-001**: The system MUST do alpha, per FR-042.
296
+ P
297
+ ;;
298
+ # TR001 reads `specs/constitution.md` relative to the working directory, and reports
299
+ # ⊘ when it is absent. The probe needs one present: here the criterion fails because
300
+ # the spec documents no Constitution Constraints while the constitution exists.
301
+ TR001-falsifying) cat <<'P'
302
+ ## Requirements *(mandatory)*
303
+
304
+ - **FR-001**: The system MUST do the thing.
305
+ P
306
+ ;;
307
+ # ⚠️ A LABELLED name (`external service`), not the unlabelled functional-layer form.
308
+ # Both are real, but only the labelled one is inside the criterion's declared
309
+ # vocabulary — the unlabelled case is a documented limit that reports ⊘ by design
310
+ # (ISS-150 / research.md → Addendum (T191)). Feeding it here would test the declared
311
+ # limit, not "is this criterion ever able to say no".
312
+ TR002-falsifying) cat <<'P'
313
+ ## Requirements *(mandatory)*
314
+
315
+ - **FR-001**: The system MUST call the external service to settle the order.
316
+ P
317
+ ;;
318
+ *) return 1 ;;
319
+ esac
320
+ }
321
+
322
+ CRITERIA="
323
+ CQ001|commands/specpro.qc.md|check_cq001_implementation_details
324
+ CQ003|commands/specpro.qc.md|check_cq003_functional_not_technical
325
+ CP001|commands/specpro.qc.md|check_cp001_mandatory_sections
326
+ CP002|commands/specpro.qc.md|check_cp002_acceptance_scenarios
327
+ CP003|commands/specpro.qc.md|check_cp003_scenarios_functional
328
+ CS001|commands/specpro.qc.md|check_cs001_us_consistency
329
+ CS002|commands/specpro.qc.md|check_cs002_fr_consistency
330
+ CS003|commands/specpro.qc.md|check_cs003_us_fr_mapping
331
+ CS004|commands/specpro.qc.md|check_cs004_reference_integrity
332
+ TR001|commands/specpro.qc.md|check_tr001_constitution_constraints
333
+ TR002|commands/specpro.qc.md|check_tr002_external_dependencies
334
+ "
335
+
336
+ # run_criterion <criterion-id> <carrier> <fn> <probe-kind> — the criterion's own output.
337
+ # Exit 2 = not executable (the criterion could not be reached or its probe is unregistered).
338
+ #
339
+ # ⚠️ The write-back functions the criteria call are stubbed: this harness judges the
340
+ # CRITERION's verdict, and a probe that tripped the auto-fix path would edit its own input.
341
+ run_criterion() {
342
+ local cid=$1 carrier=$2 fn=$3 kind=$4
343
+ local probe; probe=$(crit_probe "$cid" "$kind") || return 2
344
+ local block; block=$(extract_fence "$BASE/$carrier" "$fn")
345
+ [ -n "$block" ] || return 2
346
+ local f; f=$(mktemp)
347
+ printf '%s\n' "$probe" > "$f"
348
+ # ⚠️ `cd "$BASE"` and an ABSOLUTE `$SPEC_TEMPLATE` are both load-bearing, and their
349
+ # absence was a false positive of this harness rather than a finding about the criteria:
350
+ # CP001 and TR002 read `$SPEC_TEMPLATE` (unset ⇒ "" ⇒ "the template declares no
351
+ # mandatory sections"), and TR001 reads `specs/constitution.md` relative to the working
352
+ # directory. Run from anywhere but BASE, and the harness reports three criteria as
353
+ # ⊘/wrong for reasons that have nothing to do with the criteria. The fixture suite only
354
+ # masked it because the parent test happens to run from the repository root.
355
+ ( cd "$BASE" || exit 9
356
+ FEATURE_SPEC="$f" SPEC_TEMPLATE="$BASE/.specpro/templates/spec-template.md" \
357
+ TARGET_FRS=all SCOPE=full bash -c '
358
+ eval "$1"
359
+ PASSED=0; FAILED=0; SKIPPED=0; FAILED_ITEMS=(); AUTO_FIXED_COUNT=0
360
+ NEED_CLARIFY=false
361
+ auto_fix_implementation_details() { return 0; }
362
+ add_to_clarifications() { return 0; }
363
+ update_checklist() { return 0; }
364
+ save_qc_cache() { return 0; }
365
+ "$2" 2>/dev/null
366
+ ' _ "$block" "$fn" )
367
+ rm -f "$f"
368
+ }
369
+
370
+ # ⚠️ The registry is SHARED with shape 6, and that is deliberate: both ask a question about
371
+ # "every criterion the carrier declares", and two registries would drift. The consequence is
372
+ # that shape 1 must say what it can and cannot reach — **only the criteria with a COMPLIANT
373
+ # probe are probed here**; the rest are reported once, as a coverage fact, not as a run of
374
+ # per-criterion ⊘ lines that read like failures.
375
+ #
376
+ # ⚠️ It previously reported those as "not executable — '$fn' is not reachable in $carrier",
377
+ # which was simply untrue: the criterion is reachable, the PROBE is what is missing. A ⊘
378
+ # whose stated reason is wrong is worse than no line at all — the reader checks the claim,
379
+ # finds the function present, and stops trusting the report.
380
+ S1_N=0
381
+ S1_UNPROBED=""
382
+ S1_UNPROBED_N=0
383
+ while IFS='|' read -r cid carrier fn; do
384
+ [ -n "${cid:-}" ] || continue
385
+ S1_N=$((S1_N + 1))
386
+ verdict=$(run_criterion "$cid" "$carrier" "$fn" compliant)
387
+ if [ $? -eq 2 ]; then
388
+ S1_UNPROBED="$S1_UNPROBED $cid"
389
+ S1_UNPROBED_N=$((S1_UNPROBED_N + 1))
390
+ continue
391
+ fi
392
+ if printf '%s' "$verdict" | grep -q '✗'; then
393
+ hit "SHAPE-1 $cid: reports on a compliant probe where the keywords are substrings only"
394
+ printf '%s\n' "$verdict" | grep '✗' | head -2 | sed 's/^/ /'
395
+ else
396
+ ok "SHAPE-1 $cid: compliant probe reported clean"
397
+ fi
398
+ done <<EOF
399
+ $CRITERIA
400
+ EOF
401
+ if [ "$S1_UNPROBED_N" -gt 0 ]; then
402
+ unreceived "T124" "SHAPE-1: $S1_UNPROBED_N of $S1_N criteria have no COMPLIANT probe — unproven, not clean:$S1_UNPROBED"
403
+ fi
404
+ [ "$S1_N" -gt 0 ] || skip "SHAPE-1: no criterion is registered"
405
+
406
+ # =======================================================================================
407
+ # SHAPE 2 — mechanism written but not wired into the execution chain
408
+ # =======================================================================================
409
+ # 判据: 缺任意一处即命中. A claim of the form "X is enforced by Y" is a mechanism; if Y
410
+ # does not actually invoke X, the claim is a statement with no enforcement point. Each
411
+ # registry row names a CLAIM and the artefacts it points at; the detection resolves the
412
+ # claim against the files, not against the prose that makes it.
413
+ #
414
+ # ⚠️ Registered as rows rather than derived from prose: "which four points a rule claims"
415
+ # is not recoverable from the repository by any regex, and a check that guessed would fire
416
+ # on every rule that wires itself differently — see this file's header on that trade.
417
+ echo " [2] mechanism written but not wired into the execution chain"
418
+
419
+ S2_CLAIMS="
420
+ verify-ledger-enforcement|commands/specpro.implement.md|verify-ledger|scripts/git-hooks/pre-commit|verify-ledger
421
+ verify-deployment-enforcement|CLAUDE.md|verify-deployment|scripts/git-hooks/pre-commit|verify-deployment
422
+ "
423
+ S2_N=0
424
+ while IFS='|' read -r cid claim_file subject enforcer inv; do
425
+ [ -n "${cid:-}" ] || continue
426
+ S2_N=$((S2_N + 1))
427
+ if [ ! -f "$BASE/$claim_file" ]; then
428
+ skip "SHAPE-2 $cid: not executable — claim carrier $claim_file is absent"
429
+ continue
430
+ fi
431
+ if ! grep -q "$subject" "$BASE/$claim_file" 2>/dev/null; then
432
+ skip "SHAPE-2 $cid: the claim is no longer made in $claim_file (registry row is stale)"
433
+ continue
434
+ fi
435
+ if [ ! -f "$BASE/$enforcer" ]; then
436
+ hit "SHAPE-2 $cid: $claim_file claims enforcement by $enforcer, which does not exist"
437
+ continue
438
+ fi
439
+ if grep -q "$inv" "$BASE/$enforcer" 2>/dev/null; then
440
+ ok "SHAPE-2 $cid: $enforcer really invokes '$inv'"
441
+ else
442
+ hit "SHAPE-2 $cid: $enforcer does not invoke '$inv' — the claim has no enforcement point"
443
+ fi
444
+ done <<EOF
445
+ $S2_CLAIMS
446
+ EOF
447
+ [ "$S2_N" -gt 0 ] || skip "SHAPE-2: no enforcement claim is registered"
448
+
449
+ # --- the consumer side: is a consumer NAMED where the artifact is defined? ---------------
450
+ # 判据 (Q2's OTHER half — `ISS-174` / `T212`): 宪法 Q2 要求「新增产物时必须**同时指定**其负责
451
+ # 命令与**具名消费者**」。⚠️ **写入侧**有强制点(上面那两行:pre-commit 真的调用核验体);
452
+ # **消费者侧此前什么都没有** —— 而 `S2_CLAIMS` 问的是另一个问题("声称的强制者是否真的调用
453
+ # 它"),把两者塞进同一个表会让"命令有没有声明该产物"与"产物有没有人读"互相冒充。
454
+ #
455
+ # ⚠️ 注册的对象是「**产物 ↔ 它的定义载体**」这一**对**,不是产物单侧。单侧那一半
456
+ # ("这个字在仓库里出现过吗")是**形态 1**(字面匹配求语义结论)—— 而本文件的 SHAPE 1
457
+ # 存在的意义就是查它。
458
+ #
459
+ # ⚠️ 今天每一行都是 `carrier == artifact`(这些产物**在自身**里点名消费者)。该列**不是装饰**:
460
+ # 它是"消费者被写在**契约**里而不是产物里"那一行要用的槽位,而这样的行**今天还没有**。
461
+ #
462
+ # ⚠️ **一次命中证明了什么、没证明什么**:载体里有 ≥1 行匹配,回答的是「**这里点名了消费者**」;
463
+ # **不**回答「这个产物的每一部分都有」—— 后者是**形态 10**(一般规则只以实例形式存在)的
464
+ # 判据,是另一处检查。写在这里,是因为把它当成后者读的人**会停止寻找**。
465
+ S2_CONSUMER_ROWS="
466
+ ledger-sections|specs/contracts/ledger.md|specs/contracts/ledger.md|^## Sections and their consumers
467
+ plan-purpose-consumers|specs/plan.md|specs/plan.md|^\*Purpose:.*Consumers:
468
+ "
469
+ S2C_N=0
470
+ while IFS='|' read -r cid artifact carrier cpattern; do
471
+ [ -n "${cid:-}" ] || continue
472
+ S2C_N=$((S2C_N + 1))
473
+ if [ ! -f "$BASE/$artifact" ]; then
474
+ skip "SHAPE-2 $cid: not executable — the artifact $artifact is absent"
475
+ continue
476
+ fi
477
+ if [ ! -f "$BASE/$carrier" ]; then
478
+ hit "SHAPE-2 $cid: $artifact names no consumer — its definition carrier $carrier does not exist"
479
+ continue
480
+ fi
481
+ if [ "$(count_of "$BASE/$carrier" "$cpattern")" -gt 0 ]; then
482
+ ok "SHAPE-2 $cid: $artifact names its consumer(s) in $carrier"
483
+ else
484
+ hit "SHAPE-2 $cid: $artifact names no consumer in $carrier — Q2 requires one"
485
+ fi
486
+ done <<EOF
487
+ $S2_CONSUMER_ROWS
488
+ EOF
489
+ [ "$S2C_N" -gt 0 ] || skip "SHAPE-2: no consumer-side row is registered (Q2's consumer half is unproven, not clean)"
490
+
491
+ # =======================================================================================
492
+ # SHAPE 3 — the same fact in two places, and they disagree
493
+ # =======================================================================================
494
+ # 判据: 取文不同即命中. Each row names one fact and the two carriers that state it; the
495
+ # detection extracts each statement's VALUE and compares, so the two never have to be
496
+ # re-read by a human to notice they have parted.
497
+ #
498
+ # ⚠️ Rows are limited to facts that NO existing verifier already asserts. Restating an
499
+ # asserted fact here would create the very thing this shape is about — a second source.
500
+ # (The shape-table sync between `spec.md` and `plan.md`, for one, is already asserted by
501
+ # `scripts/git-hooks/pre-commit`; re-checking it here would be that mistake.)
502
+ echo " [3] the same fact in two places, and they disagree"
503
+
504
+ S3_FACTS="
505
+ analyze-pass-count|commands/specpro.analyze.md|^#### ([A-P])\. |count|CLAUDE.md|([0-9]+) 个检测通道
506
+ "
507
+ S3_N=0
508
+ while IFS='|' read -r fid afile are arity bfile bre; do
509
+ [ -n "${fid:-}" ] || continue
510
+ S3_N=$((S3_N + 1))
511
+ if [ ! -f "$BASE/$afile" ] || [ ! -f "$BASE/$bfile" ]; then
512
+ skip "SHAPE-3 $fid: not executable — a carrier is absent"
513
+ continue
514
+ fi
515
+ if [ "$arity" = "count" ]; then av=$(count_of "$BASE/$afile" "$are"); else av=$(grep -oE "$are" "$BASE/$afile" 2>/dev/null | head -1 || true); fi
516
+ bv=$(grep -oE "$bre" "$BASE/$bfile" 2>/dev/null | grep -oE '[0-9]+' | head -1 || true)
517
+ if [ -z "$av" ] || [ -z "$bv" ]; then
518
+ skip "SHAPE-3 $fid: not executable — one side states no value (a form change, not agreement)"
519
+ continue
520
+ fi
521
+ if [ "$av" = "$bv" ]; then
522
+ ok "SHAPE-3 $fid: both carriers state $av"
523
+ else
524
+ hit "SHAPE-3 $fid: $afile says $av, $bfile says $bv"
525
+ fi
526
+ done <<EOF
527
+ $S3_FACTS
528
+ EOF
529
+ [ "$S3_N" -gt 0 ] || skip "SHAPE-3: no fact pair is registered"
530
+
531
+ # =======================================================================================
532
+ # SHAPE 4 — the report disagrees with what is actually there
533
+ # =======================================================================================
534
+ # 判据: 不符即命中. A report states a NUMBER; the detection RE-DERIVES that number from the
535
+ # artifact the report is about and compares. Re-running the judgement is the whole point:
536
+ # reading the report back to itself proves nothing (形态 4 is exactly that confusion).
537
+ echo " [4] the report disagrees with what is actually there"
538
+
539
+ S4_ROWS="
540
+ checklist-total|specs/checklists/requirements.md|Total Items|\*\*Total Items\*\*: ([0-9]+)|unfenced:^[[:space:]]*- \[[ x]\] \*\*Status\*\*:[[:space:]]*[^[:space:]]+
541
+ checklist-passed|specs/checklists/requirements.md|Passed|\*\*Passed\*\*: ([0-9]+)|unfenced:^[[:space:]]*- \[[ x]\] \*\*Status\*\*:[[:space:]]*x[[:space:]]*$
542
+ checklist-failed|specs/checklists/requirements.md|Failed|\*\*Failed\*\*: ([0-9]+)|unfenced:^[[:space:]]*- \[[ x]\] \*\*Status\*\*:[[:space:]]*✗
543
+ req-template-total|templates/requirements-template.md|Total Items|\*\*Total Items\*\*: ([0-9]+)|^### [A-Z]{2}[0-9]{3}:
544
+ "
545
+ S4_N=0
546
+ while IFS='|' read -r rid rfile label claim_re count_re; do
547
+ [ -n "${rid:-}" ] || continue
548
+ S4_N=$((S4_N + 1))
549
+ if [ ! -f "$BASE/$rfile" ]; then
550
+ skip "SHAPE-4 $rid: not executable — report $rfile is absent"
551
+ continue
552
+ fi
553
+ stated=$(grep -oE "$claim_re" "$BASE/$rfile" 2>/dev/null | grep -oE '[0-9]+' | head -1 || true)
554
+ # ⚠️ `unfenced:` 前缀 = 这一行必须**跳过围栏内的行**(见 `count_of_unfenced` 的理由)
555
+ if [ "${count_re#unfenced:}" != "$count_re" ]; then
556
+ actual=$(count_of_unfenced "$BASE/$rfile" "${count_re#unfenced:}")
557
+ else
558
+ actual=$(count_of "$BASE/$rfile" "$count_re")
559
+ fi
560
+ if [ -z "$stated" ]; then
561
+ skip "SHAPE-4 $rid: not executable — the report states no '$label' value"
562
+ continue
563
+ fi
564
+ if [ "$stated" = "$actual" ]; then
565
+ ok "SHAPE-4 $rid: report says $stated, re-derived $actual"
566
+ else
567
+ hit "SHAPE-4 $rid: report says $label=$stated, re-derived $actual"
568
+ fi
569
+ done <<EOF
570
+ $S4_ROWS
571
+ EOF
572
+ [ "$S4_N" -gt 0 ] || skip "SHAPE-4: no report claim is registered"
573
+
574
+ # =======================================================================================
575
+ # SHAPE 5 — the deliverable is there, the record that receives it is not
576
+ # =======================================================================================
577
+ # 判据: 无承接即命中. Every artifact file must be named by something that takes delivery
578
+ # of it — a task or a ledger entry. A file nobody receives is a file nobody will notice
579
+ # the loss of.
580
+ #
581
+ # ⚠️ The corpus is the two artifacts that carry delivery records, and the needle is the
582
+ # BASENAME (a full path in the record is not required — records cite by name).
583
+ echo " [5] the deliverable is there, the record that receives it is not"
584
+
585
+ S5_GLOBS="scripts/bash/*.sh
586
+ tools/*.sh
587
+ tools/*.md
588
+ "
589
+ S5_LEDGER="$BASE/specs/implement_issues.md"
590
+ S5_TASKS="$BASE/specs/tasks.md"
591
+ S5_N=0
592
+ # ⚠️ With NEITHER index present the corpus is empty, and an empty corpus is not "nothing
593
+ # receives it" — it is "there is no list of receivers". The first version of this check
594
+ # reported every file as unclaimed in that state, which is a finding about the fixture
595
+ # tree, not about the project (the same confusion the ledger's §0 guard exists to stop).
596
+ S5_RECEIVERS=0
597
+ for corpus in "$S5_TASKS" "$S5_LEDGER"; do [ -f "$corpus" ] && S5_RECEIVERS=$((S5_RECEIVERS + 1)); done
598
+ if [ "$S5_RECEIVERS" -eq 0 ]; then
599
+ skip "SHAPE-5: not executable — neither tasks.md nor the ledger exists, so nothing can receive anything"
600
+ while IFS= read -r glob; do
601
+ [ -n "$glob" ] || continue
602
+ for f in $BASE/$glob; do [ -f "$f" ] && S5_N=$((S5_N + 1)); done
603
+ done <<EOF
604
+ $S5_GLOBS
605
+ EOF
606
+ else
607
+ while IFS= read -r glob; do
608
+ [ -n "$glob" ] || continue
609
+ for f in $BASE/$glob; do
610
+ [ -f "$f" ] || continue
611
+ S5_N=$((S5_N + 1))
612
+ name=$(basename "$f")
613
+ found=0
614
+ for corpus in "$S5_TASKS" "$S5_LEDGER"; do
615
+ [ -f "$corpus" ] || continue
616
+ if grep -qF "$name" "$corpus" 2>/dev/null; then found=1; fi
617
+ done
618
+ if [ "$found" = 1 ]; then
619
+ ok "SHAPE-5 $name: named by a task or a ledger entry"
620
+ else
621
+ hit "SHAPE-5 $name: no task and no ledger entry receives it"
622
+ fi
623
+ done
624
+ done <<EOF
625
+ $S5_GLOBS
626
+ EOF
627
+ fi
628
+ [ "$S5_N" -gt 0 ] || skip "SHAPE-5: the registered globs matched no artifact — the scope is the finding"
629
+
630
+ # =======================================================================================
631
+ # SHAPE 6 — the criterion is voided by the way it is written
632
+ # =======================================================================================
633
+ # 判据: 判据恒真即命中. A criterion is only a criterion if there is an input on which it
634
+ # says NO. Each registered criterion must therefore carry a REGISTERED falsifying case —
635
+ # and the registry is checked against the fixture corpus, so "it has one" is a lookup, not
636
+ # a recollection. A criterion with no falsifying case is reported as unproven; it is NOT
637
+ # reported as sound, because an always-true criterion and an absent one read the same.
638
+ echo " [6] the criterion is voided by the way it is written"
639
+
640
+ # ⚠️ The declared set is DERIVED from `run_qc_check()`'s own body, never hand-listed. A
641
+ # hand-listed set fails by omission, and the omission is invisible: a criterion that was
642
+ # added last week and given no falsifying case simply does not appear in the report — the
643
+ # same "a list of this kind fails by omission" problem the parity suite solved by
644
+ # comparing its covered set against the directory.
645
+ S6_CARRIER="commands/specpro.qc.md"
646
+ S6_DECLARED=$(extract_fence "$BASE/$S6_CARRIER" run_qc_check 2>/dev/null \
647
+ | grep -oE '^ check_[a-z0-9_]+' | sed 's/^ //' | sort -u || true)
648
+
649
+ if [ -z "$S6_DECLARED" ]; then
650
+ skip "SHAPE-6: not executable — no criterion is declared in $S6_CARRIER's run_qc_check"
651
+ else
652
+ # Every criterion the carrier declares must be exercised by a falsifying case.
653
+ unproven=""
654
+ unproven_n=0
655
+ while IFS= read -r fn; do
656
+ [ -n "$fn" ] || continue
657
+ cid=$(printf '%s' "$fn" | sed -E 's/^check_([a-z]+[0-9]+)_.*/\1/' | tr 'a-z' 'A-Z')
658
+ if ! printf '%s\n' "$CRITERIA" | grep -qE "^$cid\|"; then
659
+ unproven="$unproven $cid"
660
+ unproven_n=$((unproven_n + 1))
661
+ continue
662
+ fi
663
+ carrier=$(printf '%s\n' "$CRITERIA" | grep -E "^$cid\|" | head -1 | cut -d'|' -f2)
664
+ fn_name=$(printf '%s\n' "$CRITERIA" | grep -E "^$cid\|" | head -1 | cut -d'|' -f3)
665
+ verdict=$(run_criterion "$cid" "$carrier" "$fn_name" falsifying)
666
+ if [ $? -eq 2 ]; then
667
+ skip "SHAPE-6 $cid: not executable — the falsifying probe or the criterion is unreachable"
668
+ elif printf '%s' "$verdict" | grep -q '✗'; then
669
+ ok "SHAPE-6 $cid: judges fail on an input that falsifies it"
670
+ else
671
+ hit "SHAPE-6 $cid: did NOT judge fail on a falsifying input — always-true, or never ran"
672
+ printf '%s\n' "$verdict" | head -2 | sed 's/^/ /'
673
+ fi
674
+ done <<EOF
675
+ $S6_DECLARED
676
+ EOF
677
+ if [ "$unproven_n" -gt 0 ]; then
678
+ # ⚠️ Reported as ⊘ rather than ✗, and the distinction is deliberate: an unproven
679
+ # criterion has not been SHOWN to be always-true — there is simply no registered
680
+ # input that would tell the two apart. Folding it into the hit count would inflate
681
+ # the verdict with something nobody has measured; dropping it would hide it.
682
+ #
683
+ # ⚠️ **The next step belongs to a task that NAMES this file** (ISS-151). This line
684
+ # previously said the obligation was `T124`'s — and `T124` has no Location, so it
685
+ # could not write here; the duty had been assigned to a party without the right to
686
+ # discharge it, and the ⊘ would have pointed at a finished task forever. When this
687
+ # message appears, register or extend a task whose Location lists
688
+ # `scripts/bash/verify-shapes.sh` AND `scripts/powershell/verify-shapes.ps1`
689
+ # (both, or the parity suite fails — ISS-105).
690
+ unreceived "T124" "SHAPE-6: $unproven_n declared criterion(s) have no falsifying probe — unproven, not clean:$unproven"
691
+ fi
692
+ fi
693
+
694
+ # =======================================================================================
695
+ # SHAPE 7 — self-witnessing (the evidence's producer is the judged implementation)
696
+ # =======================================================================================
697
+ # 判据: 自证即命中. Each row names an artifact whose assertions rest on a citation, and the
698
+ # producer that citation must NOT be. When the only witness for a behaviour is the code
699
+ # that implements it, a passing read proves that the two agree with each other.
700
+ echo " [7] self-witnessing (the evidence's producer is the judged implementation)"
701
+
702
+ # ⚠️ **Only `Source:` citation LINES are examined — a mention is not a citation.**
703
+ # The first version of this check matched the bare path anywhere in the artefact and duly
704
+ # reported both contracts as self-witnessing. They are not: one says the implementation is
705
+ # the contract's CARRIER, which is a statement of ownership, and the other names the
706
+ # verifier as the ledger's authority, which is a statement about a checker. Reporting a
707
+ # mention as a witness is TOOL-009's failure — 点名与提及不同形 — and it would have made
708
+ # this detector fire on innocent files until a reader turned it off.
709
+ S7_CITE='^[[:space:]]*>[[:space:]]*\**[Ss]ource\**[[:space:]]*[::]'
710
+ # ⚠️ A row whose subject is NOT the surface the contract judges would misfire, and one did:
711
+ # `agent-registry.md` documents a VENDORED EXTERNAL library, so its Source: lines citing
712
+ # `src/specify_cli/` are attributions to the thing it describes — correct by construction.
713
+ # The row was wrong, not the check, and it is removed rather than the check loosened.
714
+ S7_ROWS="
715
+ dev-shellcheck-contract|specs/contracts/dev-shellcheck-tool.md|tools/verify-doc-shell\.sh
716
+ ledger-contract|specs/contracts/ledger.md|scripts/bash/verify-ledger\.sh
717
+ "
718
+ S7_N=0
719
+ while IFS='|' read -r aid afile subject; do
720
+ [ -n "${aid:-}" ] || continue
721
+ S7_N=$((S7_N + 1))
722
+ if [ ! -f "$BASE/$afile" ]; then
723
+ skip "SHAPE-7 $aid: not executable — artifact $afile is absent"
724
+ continue
725
+ fi
726
+ cites=$(grep -cE "$S7_CITE" "$BASE/$afile" 2>/dev/null || true)
727
+ : "${cites:=0}"
728
+ if [ "$cites" -eq 0 ]; then
729
+ unreceived "ISS-178" "SHAPE-7 $aid: not executable — $afile carries no Source: citation, so there is no evidence chain to audit"
730
+ continue
731
+ fi
732
+ selfwitness=$(grep -E "$S7_CITE" "$BASE/$afile" 2>/dev/null | grep -E "$subject" || true)
733
+ if [ -n "$selfwitness" ]; then
734
+ hit "SHAPE-7 $aid: a Source: line attributes the fact to the surface being judged"
735
+ printf '%s\n' "$selfwitness" | head -1 | cut -c1-120 | sed 's/^/ /'
736
+ else
737
+ ok "SHAPE-7 $aid: $cites Source: citation(s), none of them self-witnessing"
738
+ fi
739
+ done <<EOF
740
+ $S7_ROWS
741
+ EOF
742
+ [ "$S7_N" -gt 0 ] || skip "SHAPE-7: no artifact is registered"
743
+
744
+ # =======================================================================================
745
+ # SHAPE 8 — position decides ownership and there is no boundary sentinel
746
+ # =======================================================================================
747
+ # 判据: 无哨兵即命中. A structure where "where a row sits" decides which section owns it
748
+ # needs a marker that makes an out-of-bounds placement VISIBLE — otherwise the misfiled
749
+ # row still looks filed (the ledger's end-of-file sentinel is one instance of the
750
+ # marker, not the whole class: a section-boundary assertion counts too, ISS-61).
751
+ echo " [8] position decides ownership and there is no boundary sentinel"
752
+
753
+ # ⚠️ **The marker is a MARKER, matched by its form — not the phrase "end-of-file sentinel"
754
+ # matched anywhere in the file.** The first version of this check looked for the phrase, and
755
+ # `specs/checklists/requirements.md` line 33 carries it in a sentence that says the sentinel
756
+ # is in the LEDGER and NOT in this file. The check read that sentence and reported the
757
+ # sentinel as present — a literal match turned into a semantic conclusion, i.e. **shape 1
758
+ # committed by the shape-1 detector**. The form below (`<!-- … sentinel…`) is the marker
759
+ # itself, and prose about it no longer satisfies it.
760
+ # ⚠️ **The marker FORM is declared per row, and the two carriers do not share one.** The
761
+ # ledger's sentinel is the bare `-->` closer (`verify-ledger.sh` §1 reads `^-->$`); the
762
+ # requirements checklist's is an `<!-- End-of-file sentinel: …` comment opener whose closer
763
+ # is deliberately NOT alone on its line (the template says why). A single form would have
764
+ # reported one of the two as sentinel-less — a finding about my regex, not about the repo.
765
+ S8_ROWS="
766
+ ledger|specs/implement_issues.md|^-->$
767
+ requirements-checklist|specs/checklists/requirements.md|^[[:space:]]*<!--[[:space:]]*[Ee]nd-of-file sentinel
768
+ "
769
+ S8_N=0
770
+ while IFS='|' read -r sid sfile marker; do
771
+ [ -n "${sid:-}" ] || continue
772
+ S8_N=$((S8_N + 1))
773
+ if [ ! -f "$BASE/$sfile" ]; then
774
+ skip "SHAPE-8 $sid: not executable — carrier $sfile is absent"
775
+ continue
776
+ fi
777
+ if grep -qE "$marker" "$BASE/$sfile" 2>/dev/null; then
778
+ ok "SHAPE-8 $sid: $sfile declares a boundary sentinel"
779
+ else
780
+ hit "SHAPE-8 $sid: $sfile owns rows by position and declares no boundary sentinel"
781
+ fi
782
+ done <<EOF
783
+ $S8_ROWS
784
+ EOF
785
+ [ "$S8_N" -gt 0 ] || skip "SHAPE-8: no position-owned structure is registered"
786
+
787
+ # =======================================================================================
788
+ # SHAPE 9 — a compatibility layer for a gap that is not there
789
+ # =======================================================================================
790
+ # 判据: 缺口不成立即命中. Each row names a compatibility layer and the gap it exists for;
791
+ # the detection PROBES the gap. A layer whose reason has disappeared is not harmless — it
792
+ # is a second path that the next reader must evaluate, and it will be kept up to date
793
+ # forever by nobody.
794
+ echo " [9] a compatibility layer for a gap that is not there"
795
+
796
+ S9_ROWS="
797
+ create-new-feature|scripts/bash/create-new-feature.sh|commands/specpro.specify.md
798
+ "
799
+ S9_N=0
800
+ while IFS='|' read -r lid lfile consumer; do
801
+ [ -n "${lid:-}" ] || continue
802
+ S9_N=$((S9_N + 1))
803
+ if [ ! -f "$BASE/$lfile" ]; then
804
+ skip "SHAPE-9 $lid: not executable — $lfile is absent (the layer may already be gone)"
805
+ continue
806
+ fi
807
+ if [ ! -f "$BASE/$consumer" ]; then
808
+ skip "SHAPE-9 $lid: not executable — the consumer named for the gap is absent"
809
+ continue
810
+ fi
811
+ if grep -qF "$(basename "$lfile" .sh)" "$BASE/$consumer" 2>/dev/null; then
812
+ ok "SHAPE-9 $lid: the gap is real — $consumer still calls it"
813
+ else
814
+ hit "SHAPE-9 $lid: $consumer no longer calls it — the gap it exists for is gone"
815
+ fi
816
+ done <<EOF
817
+ $S9_ROWS
818
+ EOF
819
+ [ "$S9_N" -gt 0 ] || skip "SHAPE-9: no compatibility layer is registered"
820
+
821
+ # =======================================================================================
822
+ # SHAPE 10 — a general rule that exists only as an instance
823
+ # =======================================================================================
824
+ # 判据: 缺一处即命中. A rule stated in some of the artifacts it governs is a rule whose
825
+ # scope is decided by whoever last edited it. Each row declares the rule's OWN claimed
826
+ # scope, and the detection requires the statement in every file of that scope.
827
+ echo " [10] a general rule that exists only as an instance"
828
+
829
+ S10_ROWS="
830
+ detect-then-ask|detect-then-ask|commands/specpro.implement.md commands/specpro.plan.md commands/specpro.tasks.md commands/specpro.test-plan.md commands/specpro.specify.md
831
+ "
832
+ S10_N=0
833
+ while IFS='|' read -r rid statement scope; do
834
+ [ -n "${rid:-}" ] || continue
835
+ S10_N=$((S10_N + 1))
836
+ missing=""
837
+ seen=0
838
+ for f in $scope; do
839
+ [ -f "$BASE/$f" ] || continue
840
+ seen=$((seen + 1))
841
+ if ! grep -qi "$statement" "$BASE/$f" 2>/dev/null; then missing="$missing $f"; fi
842
+ done
843
+ if [ "$seen" -eq 0 ]; then
844
+ skip "SHAPE-10 $rid: not executable — none of the declared scope exists"
845
+ elif [ -n "$missing" ]; then
846
+ hit "SHAPE-10 $rid: stated in $((seen - $(echo $missing | wc -w | tr -d ' '))) of $seen declared files; missing:$missing"
847
+ else
848
+ ok "SHAPE-10 $rid: stated in all $seen files of its declared scope"
849
+ fi
850
+ done <<EOF
851
+ $S10_ROWS
852
+ EOF
853
+ [ "$S10_N" -gt 0 ] || skip "SHAPE-10: no rule is registered"
854
+
855
+ # =======================================================================================
856
+ # SHAPE 11 — the criterion's conclusion is not a function of its input
857
+ # =======================================================================================
858
+ # 判据: 结论不恒定即命中. The same input, run N times, MUST give the same answer. This is
859
+ # the ISS-62 class: a `grep -q` that exits early leaves its writer with SIGPIPE, and under
860
+ # `pipefail` the pipeline reports 141 — so a real finding appears and disappears between
861
+ # runs. A non-deterministic criterion is worse than a missing one: it is a missing one
862
+ # that occasionally reports.
863
+ echo " [11] the criterion's conclusion is not a function of its input"
864
+
865
+ S11_ROWS="
866
+ verify-ledger|scripts/bash/verify-ledger.sh
867
+ "
868
+ S11_N=0
869
+ S11_RUNS=3
870
+ while IFS='|' read -r cid runner; do
871
+ [ -n "${cid:-}" ] || continue
872
+ S11_N=$((S11_N + 1))
873
+ if [ ! -f "$BASE/$runner" ]; then
874
+ skip "SHAPE-11 $cid: not executable — runner $runner is absent"
875
+ continue
876
+ fi
877
+ first=""
878
+ varies=0
879
+ i=1
880
+ while [ "$i" -le "$S11_RUNS" ]; do
881
+ out=$(cd "$BASE" && bash "$runner" 2>&1)
882
+ if [ -z "$first" ]; then first="$out"; elif [ "$out" != "$first" ]; then varies=1; fi
883
+ i=$((i + 1))
884
+ done
885
+ if [ "$varies" = 1 ]; then
886
+ hit "SHAPE-11 $cid: $S11_RUNS runs on one input disagreed — the verdict is not a function of the input"
887
+ else
888
+ ok "SHAPE-11 $cid: $S11_RUNS runs on one input agreed"
889
+ fi
890
+ done <<EOF
891
+ $S11_ROWS
892
+ EOF
893
+ [ "$S11_N" -gt 0 ] || skip "SHAPE-11: no criterion runner is registered"
894
+
895
+ # =======================================================================================
896
+ # COVERAGE — the register and the detections must be the same set
897
+ # =======================================================================================
898
+ # 判据 (形态 4 applied to this file itself): the report states how many shapes were
899
+ # checked, and that number MUST be re-derived — from the detections this file defines,
900
+ # and from the REGISTER the claim is about.
901
+ #
902
+ # ⚠️ Why this block exists (`ISS-171` / `T211`): the verdict used to read
903
+ # `shapes: 11 checked` — a LITERAL. Registering a 12th shape (in both carriers, so
904
+ # pre-commit's own sync invariant still holds) produced `11 checked` and `rc=0`: a shape
905
+ # with no detection at all, reported as completely covered. That is 形态 6's shape — the
906
+ # step was there, but its verdict could not be anything but pass — and it is why `T132`,
907
+ # whose whole job is "every registered shape has a detection", could not fail.
908
+ #
909
+ # ⚠️ One derived number, against the register — and it MUST be the register, not this
910
+ # file's own count. Comparing the report only to its own detections would catch an edited
911
+ # comment and nothing else; it is the REGISTER that the report makes a claim about.
912
+ #
913
+ # ⚠️ The marker is `# SHAPE n`, and it is the SAME regex on both implementations on purpose.
914
+ # An earlier version counted the section-heading idiom instead, which is `echo " [n] …" on
915
+ # this side and `Write-Output " [n] …"` on the PowerShell side — so the two sides counted
916
+ # different things and the PowerShell half read 0. The parity suite caught it, and the fix
917
+ # is to count the one marker both files actually share.
918
+ echo " [coverage] the register and the detections must be the same set"
919
+
920
+ SELF="${BASH_SOURCE[0]}"
921
+ SHAPES_CHECKED=$(count_of "$SELF" '^# SHAPE [0-9]+')
922
+ REGISTER="$BASE/specs/plan.md"
923
+ if [ ! -f "$REGISTER" ]; then
924
+ skip "COVERAGE: not executable — the shape register $REGISTER is absent, so 'every registered shape was checked' cannot be judged"
925
+ else
926
+ SHAPES_REGISTERED=$(count_of "$REGISTER" '^\| \*\*[0-9]+\*\* \|')
927
+ if [ "$SHAPES_CHECKED" = "$SHAPES_REGISTERED" ]; then
928
+ ok "COVERAGE: $SHAPES_CHECKED detection(s) for $SHAPES_REGISTERED registered shape(s)"
929
+ else
930
+ hit "COVERAGE: the register declares $SHAPES_REGISTERED shape(s) but this file defines $SHAPES_CHECKED detection(s) — the difference has no detection"
931
+ fi
932
+ fi
933
+
934
+ # =======================================================================================
935
+ # SHAPE 12 — a silence with no receiver
936
+ # =======================================================================================
937
+ # 判据 (`spec.md` → FR-047 登记表第 12 行 · `plan.md` → Constraint 7 第 12 行,照引不重推):
938
+ # 一条**已登记**的检出,其作用域对某一**整类**对象为空(或全部为"不可行使");该 `⊘` 在
939
+ # **任务与台账里都零承接**即命中。
940
+ #
941
+ # ⚠️ **承接的匹配是「条目形状」的,不是「关键词出现」的** (`TOOL-009`): the ledger's own
942
+ # verifier already made this correction (`^- \[[x ]\] ISS-<N>:` rather than a full-text ID
943
+ # search). ⚠️ And the weaker check would pass on THIS FILE'S OWN COMMENTARY — the comments
944
+ # above name `T124` and `ISS-178` — which is the cleanest available demonstration that a
945
+ # full-text match answers a different question.
946
+ #
947
+ # ⚠️ **What a pass does and does not prove**, stated because reading it as the stronger
948
+ # claim would stop the search: it proves **the silences declared this run have receivers
949
+ # that exist as entries**. It does NOT prove that every silence declared a receiver — that
950
+ # judgement is made at the call site (`skip` vs `unreceived`) and is visible in the code,
951
+ # not decidable here.
952
+ echo " [12] a silence with no receiver"
953
+
954
+ S12_N=0
955
+ if [ -n "$UNRECEIVED" ]; then
956
+ while IFS=$'\t' read -r rcv msg; do
957
+ [ -n "${msg:-}" ] || continue
958
+ S12_N=$((S12_N + 1))
959
+ if [ -z "$rcv" ]; then
960
+ hit "SHAPE-12: a silence names no receiver — $msg"
961
+ continue
962
+ fi
963
+ if receivers_exist "$rcv"; then
964
+ ok "SHAPE-12 $rcv: receives '$msg'"
965
+ else
966
+ hit "SHAPE-12 $rcv: names a receiver that is not an entry in tasks.md or the ledger — $msg"
967
+ fi
968
+ done <<EOF
969
+ $UNRECEIVED
970
+ EOF
971
+ fi
972
+ [ "$S12_N" -gt 0 ] || skip "SHAPE-12: no silence was declared this run — nothing to receive, so nothing is proven"
973
+
974
+
975
+ # =======================================================================================
976
+ # Constraint 7's detection column — a TWO-WAY map (`T222`)
977
+ # =======================================================================================
978
+ # 判据 (`plan.md` → Constraint 7 · `spec.md` → FR-047 形态 2 的判据, 照引不重推):
979
+ # 那张表的**每一行**点名一处检出,而**每一处检出都要有一行点名它**。
980
+ #
981
+ # ⚠️ **本条补的是哪个缺口** (`ISS-192`): `S2_CLAIMS` · `S2_CONSUMER_ROWS` · `COVERAGE`
982
+ # 三处**都在本文件内部自洽,没有一处读 `plan.md`**。而 `spec.md` 的判据**明确把检出方式
983
+ # 的真源指给 `plan.md`** ⇒ 那张表**指不到任何东西时不报错**,且它自称是真源。
984
+ #
985
+ # 注册表一行三格: <形状号>|<该行的检出列里 MUST 出现的字串>|<本文件里 MUST 存在的字串>
986
+ C7_ROWS="
987
+ 1|回归用例|SHAPE 1
988
+ 2|S2_CLAIMS|S2_CLAIMS
989
+ 3|全文反查|SHAPE 3
990
+ 4|重跑|SHAPE 4
991
+ 5|承接|SHAPE 5
992
+ 6|使该判据为假的输入|SHAPE 6
993
+ 7|生产者是不是被判定的实现本身|SHAPE 7
994
+ 8|边界哨兵|SHAPE 8
995
+ 9|缺口是否真实存在|SHAPE 9
996
+ 10|同一表述|SHAPE 10
997
+ 11|连跑 N 次|SHAPE 11
998
+ 12|有谁承接它|SHAPE 12
999
+ "
1000
+ C7_PLAN="$BASE/specs/plan.md"
1001
+ C7_N=0
1002
+ if [ ! -f "$C7_PLAN" ]; then
1003
+ skip "SHAPE-2 c7-table: not executable — $C7_PLAN is absent"
1004
+ else
1005
+ while IFS='|' read -r num plan_tok script_tok; do
1006
+ [ -n "${num:-}" ] || continue
1007
+ C7_N=$((C7_N + 1))
1008
+ # (a) 正向: Constraint 7 的该行存在, 且其检出列点名了 plan_tok
1009
+ c7_row=$(grep -E "^\| \*\*${num}\*\* \|" "$C7_PLAN" | head -1)
1010
+ if [ -z "$c7_row" ]; then
1011
+ hit "SHAPE-2 c7-$num: Constraint 7 has no row for shape $num"
1012
+ continue
1013
+ fi
1014
+ if ! printf '%s' "$c7_row" | grep -q "$plan_tok"; then
1015
+ hit "SHAPE-2 c7-$num: the row's detection column does not name '$plan_tok'"
1016
+ continue
1017
+ fi
1018
+ # (b) 该处检出 MUST 在本文件里真实存在
1019
+ if ! grep -q "$script_tok" "$SELF" 2>/dev/null; then
1020
+ hit "SHAPE-2 c7-$num: the row names a detection that does not exist here — '$script_tok'"
1021
+ continue
1022
+ fi
1023
+ ok "SHAPE-2 c7-$num: names '$plan_tok' → '$script_tok' exists"
1024
+ done <<EOF
1025
+ $C7_ROWS
1026
+ EOF
1027
+ # (c) 反向: 三处计数必须相等 —— 表里的行数 · 注册表的行数 · 本文件里的形态段数。
1028
+ # ⚠️ 少了这一半, "表里多一行而注册表没有" 与 "注册表多一行而表里没有" 都读不出来。
1029
+ # ⚠️ **计数被约束在 `#### Constraint 7` 那一节之内** —— 数全文件的 `| **N** |` 行是
1030
+ # 一个**当下恰好成立**的判据(实测:那 12 行全部落在该标题下),而任何**别处**新出现的
1031
+ # 同形表格都会让它**静默**读成"表里多了一行"。锚在标题上才是它要问的那个集合。
1032
+ c7_plan_rows=$(awk '/^#### Constraint 7/ {p=1; next} p && /^#### / {p=0} p && /^\| \*\*[0-9]+\*\* \|/ {n++} END {print n+0}' "$C7_PLAN" 2>/dev/null)
1033
+ if [ "$c7_plan_rows" -ne "$C7_N" ]; then
1034
+ hit "SHAPE-2 c7-table: Constraint 7 declares $c7_plan_rows row(s), this registry maps $C7_N"
1035
+ fi
1036
+ if [ "$SHAPES_CHECKED" -ne "$C7_N" ]; then
1037
+ hit "SHAPE-2 c7-table: this file defines $SHAPES_CHECKED detection(s), this registry maps $C7_N"
1038
+ fi
1039
+ [ "$C7_N" -gt 0 ] || skip "SHAPE-2 c7-table: the registry is empty — nothing is proven"
1040
+ fi
1041
+
1042
+ # =======================================================================================
1043
+ # `specs/audit/` — NO COMMAND MAY DECLARE IT (`T221`)
1044
+ # =======================================================================================
1045
+ # 判据 (`plan.md` → Pattern 7 的边界裁定 · `specs/contracts/dev-audit-tool.md`):
1046
+ # `specs/audit/` 下的产物**不属于任何命令** —— 它们的生产者是**维护者的人工轮次**
1047
+ # (三条理由见 Pattern 7: `writes:` 的载体是 frontmatter 而 `tools/*.md` 没有 /
1048
+ # 映射是随工具分发的产品规则而 `tools/` 不参与分发 / 本类产物不与任何命令竞争)。
1049
+ # ⇒ 任何命令的 `writes:` 里出现它即**越界**。
1050
+ #
1051
+ # ⚠️ 这条判据此前只写在 `plan.md` 与契约里、**标着 `⚠️ 评审级,非硬门`** —— 本条把它接上。
1052
+ C7_AUDIT_HITS=$(grep -rn 'artifact: specs/audit' "$BASE"/commands/*.md 2>/dev/null || true)
1053
+ if [ -n "$C7_AUDIT_HITS" ]; then
1054
+ while IFS= read -r c7_line; do
1055
+ [ -n "$c7_line" ] || continue
1056
+ hit "SHAPE-2 audit-boundary: a command declares an artifact no command owns — $c7_line"
1057
+ done <<EOF
1058
+ $C7_AUDIT_HITS
1059
+ EOF
1060
+ else
1061
+ ok "SHAPE-2 audit-boundary: no command declares specs/audit/"
1062
+ fi
1063
+
1064
+ # =======================================================================================
1065
+ # Verdict
1066
+ # =======================================================================================
1067
+ echo
1068
+ echo "shapes: $SHAPES_CHECKED checked · $OKS clean · $HITS hit · $SKIPS not executable"
1069
+ if [ "$HITS" -eq 0 ]; then
1070
+ echo "✓ shapes: no registered shape has a live instance"
1071
+ # ⚠️ 覆盖声明 (FR-050): a clean run means "no known instance among the registered
1072
+ # subjects", never "the tool has no defect". The two are not the same claim, and the
1073
+ # difference is entirely in the size of the registries above.
1074
+ echo " ⚠️ coverage: this is 'no known instance' for the registered subjects only."
1075
+ echo " It is NOT a statement that the tool is free of defects. $SKIPS subject group(s)"
1076
+ echo " could not be exercised at all, and each such group is unproven, not clean."
1077
+ exit 0
1078
+ else
1079
+ echo "✗ shapes: $HITS hit(s) — each one is a live instance of its shape"
1080
+ echo " ⚠️ FR-048: 消除以重跑检出成立,不以「已修」的声明。"
1081
+ exit 1
1082
+ fi