specpro-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- specpro_cli/__init__.py +16 -0
- specpro_cli/assets/commands/specpro.analyze.md +1102 -0
- specpro_cli/assets/commands/specpro.checklist.md +335 -0
- specpro_cli/assets/commands/specpro.clarify.md +581 -0
- specpro_cli/assets/commands/specpro.constitution.md +488 -0
- specpro_cli/assets/commands/specpro.feature.md +115 -0
- specpro_cli/assets/commands/specpro.implement.md +1881 -0
- specpro_cli/assets/commands/specpro.manual-test.md +206 -0
- specpro_cli/assets/commands/specpro.plan.md +3284 -0
- specpro_cli/assets/commands/specpro.qc.md +1489 -0
- specpro_cli/assets/commands/specpro.scenarios.md +154 -0
- specpro_cli/assets/commands/specpro.specify.md +1449 -0
- specpro_cli/assets/commands/specpro.status.md +863 -0
- specpro_cli/assets/commands/specpro.tasks.md +1207 -0
- specpro_cli/assets/commands/specpro.test-implement.md +462 -0
- specpro_cli/assets/commands/specpro.test-plan.md +383 -0
- specpro_cli/assets/commands/specpro.user-manual.md +178 -0
- specpro_cli/assets/scripts/bash/check-anti-coupling.sh +293 -0
- specpro_cli/assets/scripts/bash/check-prerequisites.sh +176 -0
- specpro_cli/assets/scripts/bash/common.sh +88 -0
- specpro_cli/assets/scripts/bash/create-new-feature.sh +336 -0
- specpro_cli/assets/scripts/bash/qc-auto-fix.sh +121 -0
- specpro_cli/assets/scripts/bash/setup-plan.sh +60 -0
- specpro_cli/assets/scripts/bash/verify-cumulative-records.sh +203 -0
- specpro_cli/assets/scripts/bash/verify-deliverables-tracked.sh +147 -0
- specpro_cli/assets/scripts/bash/verify-deployment.sh +239 -0
- specpro_cli/assets/scripts/bash/verify-frontmatter-yaml.sh +63 -0
- specpro_cli/assets/scripts/bash/verify-ledger.sh +376 -0
- specpro_cli/assets/scripts/bash/verify-shapes.sh +1082 -0
- specpro_cli/assets/scripts/git-hooks/pre-commit +243 -0
- specpro_cli/assets/scripts/install-git-hooks.sh +67 -0
- specpro_cli/assets/scripts/powershell/check-anti-coupling.ps1 +249 -0
- specpro_cli/assets/scripts/powershell/check-prerequisites.ps1 +148 -0
- specpro_cli/assets/scripts/powershell/common.ps1 +95 -0
- specpro_cli/assets/scripts/powershell/create-new-feature.ps1 +229 -0
- specpro_cli/assets/scripts/powershell/qc-auto-fix.ps1 +110 -0
- specpro_cli/assets/scripts/powershell/setup-plan.ps1 +61 -0
- specpro_cli/assets/scripts/powershell/verify-cumulative-records.ps1 +133 -0
- specpro_cli/assets/scripts/powershell/verify-deliverables-tracked.ps1 +112 -0
- specpro_cli/assets/scripts/powershell/verify-deployment.ps1 +278 -0
- specpro_cli/assets/scripts/powershell/verify-frontmatter-yaml.ps1 +56 -0
- specpro_cli/assets/scripts/powershell/verify-ledger.ps1 +383 -0
- specpro_cli/assets/scripts/powershell/verify-shapes.ps1 +978 -0
- specpro_cli/assets/templates/agent-context-template.md +49 -0
- specpro_cli/assets/templates/assumptions-template.md +248 -0
- specpro_cli/assets/templates/checklist-template.md +40 -0
- specpro_cli/assets/templates/clarifications-template.md +155 -0
- specpro_cli/assets/templates/constitution-template.md +50 -0
- specpro_cli/assets/templates/feature-spec-template.md +66 -0
- specpro_cli/assets/templates/plan-overview-template.md +150 -0
- specpro_cli/assets/templates/plan-template.md +387 -0
- specpro_cli/assets/templates/protocol-golden-bytes-guide.md +195 -0
- specpro_cli/assets/templates/requirements-template.md +356 -0
- specpro_cli/assets/templates/spec-template.md +267 -0
- specpro_cli/assets/templates/tasks-template.md +252 -0
- specpro_cli/assets/templates/test-tasks-template.md +174 -0
- specpro_cli/cli/__init__.py +5 -0
- specpro_cli/cli/cmd_init.py +416 -0
- specpro_cli/cli/cmd_remove.py +122 -0
- specpro_cli/cli/entry.py +181 -0
- specpro_cli/integrations/__init__.py +36 -0
- specpro_cli/integrations/base.py +601 -0
- specpro_cli/integrations/claude/__init__.py +101 -0
- specpro_cli/integrations/copilot/__init__.py +153 -0
- specpro_cli/integrations/cursor_agent/__init__.py +51 -0
- specpro_cli/integrations/gemini/__init__.py +44 -0
- specpro_cli/integrations/opencode/__init__.py +48 -0
- specpro_cli/integrations/qodercli/__init__.py +54 -0
- specpro_cli/integrations/registry.py +88 -0
- specpro_cli/packaged/__init__.py +5 -0
- specpro_cli/packaged/sync.py +106 -0
- specpro_cli-0.1.0.dist-info/METADATA +117 -0
- specpro_cli-0.1.0.dist-info/RECORD +76 -0
- specpro_cli-0.1.0.dist-info/WHEEL +4 -0
- specpro_cli-0.1.0.dist-info/entry_points.txt +2 -0
- specpro_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,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
|