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,293 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Anti-coupling scan — Pass O, made runnable.
|
|
4
|
+
#
|
|
5
|
+
# Pass O's own text says "**The pattern table is a maintained asset, not a one-off
|
|
6
|
+
# script**" — and until this script existed, there was no table and no script, only the
|
|
7
|
+
# sentence. A pass that is transcribed rather than executed is a statement (原则 I).
|
|
8
|
+
#
|
|
9
|
+
# What it looks for: specpro's distributed assets carrying traces of the HOST project
|
|
10
|
+
# they were first developed against. Those assets ship to every user, so a trace ships
|
|
11
|
+
# with them (宪法 1.3).
|
|
12
|
+
#
|
|
13
|
+
# ⚠️ **The patterns are NOT in this file, and that is the design.** This script is a
|
|
14
|
+
# distributed asset: hardcoding a host-domain term here would BE the leak it exists to
|
|
15
|
+
# find (O3), and the same for a pinned framework or tool name (O2) — **a comment that
|
|
16
|
+
# names either one is itself an instance of the leak**, which is why neither is quoted
|
|
17
|
+
# anywhere in this file. The patterns therefore live in a
|
|
18
|
+
# project-side file — `specs/anti-coupling-patterns.txt` by default — which ships to
|
|
19
|
+
# nobody. One class is not even listed there: **foreign numeric identifiers are DERIVED**
|
|
20
|
+
# from the project's own artifacts, because "foreign" means exactly "not allocated by
|
|
21
|
+
# this project", and the project knows its own numbers.
|
|
22
|
+
#
|
|
23
|
+
# Coverage is all three classes (Pass O: "A scan covering two of them is not 'partial
|
|
24
|
+
# credit'"). A class with no patterns and no derivation is a FAILURE, not a skip — the
|
|
25
|
+
# uncovered class is the finding.
|
|
26
|
+
#
|
|
27
|
+
# Usage: scripts/bash/check-anti-coupling.sh
|
|
28
|
+
# ANTI_COUPLING_BASE=<path> scripts/bash/check-anti-coupling.sh (fixture case)
|
|
29
|
+
# Exit: 0 = no finding; 1 = at least one finding or an uncovered class
|
|
30
|
+
|
|
31
|
+
# ⚠️ NOT `set -e`: the logic reads grep's exit codes (宪法 2.2.2, second carve-out).
|
|
32
|
+
set -uo pipefail
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Repository root — resolved POSITION-INDEPENDENTLY, then TESTED (T180 / ISS-124).
|
|
36
|
+
#
|
|
37
|
+
# ⚠️ The previous form was `dirname "${BASH_SOURCE[0]}"/../..`, which is correct only
|
|
38
|
+
# from the SOURCE layout. From the deployed mirror (`.specpro/scripts/bash/…`) it
|
|
39
|
+
# resolves to `.specpro` — one level short — and every path below then names a tree
|
|
40
|
+
# that does not exist. ⚠️ The failure is silent in the worst direction: a scan over a
|
|
41
|
+
# tree it never read still prints `✓ none`, which is indistinguishable from a clean
|
|
42
|
+
# one (ISS-83's shape). Pass O itself instructs running this from the mirror position.
|
|
43
|
+
#
|
|
44
|
+
# ⚠️ **The sentinel tests the RESULT, not the method.** Whatever produced BASE, it must
|
|
45
|
+
# look like a repository root; when it does not, the script says so and names BASE —
|
|
46
|
+
# never a downstream symptom (the old failure reported "推不出编号范围", which describes
|
|
47
|
+
# an effect three steps away from the cause).
|
|
48
|
+
#
|
|
49
|
+
# Order: git (correct from BOTH positions) → the two candidate depths, each tested.
|
|
50
|
+
# A BASE that IS a root is accepted from any source — including an override: an
|
|
51
|
+
# override that names a non-root is exactly the case the sentinel exists to catch.
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
is_repo_root() { [ -d "$1/commands" ] && [ -d "$1/templates" ] && [ -d "$1/scripts" ]; }
|
|
54
|
+
|
|
55
|
+
# ⚠️ **Two sentinels, and which one applies depends on WHO named the base** (`T205` /
|
|
56
|
+
# `ISS-165`). This is not a relaxation of the guard: it is the guard asking the right
|
|
57
|
+
# question in each case.
|
|
58
|
+
#
|
|
59
|
+
# * **auto-resolution** answers "is this a repository root?" — the three-directory
|
|
60
|
+
# shape is the evidence, and it must hold because the script CHOSE the directory.
|
|
61
|
+
# * **an explicit override** answers "can I read what I am about to scan?" — the
|
|
62
|
+
# caller has already named the tree, and what this scanner actually reads is
|
|
63
|
+
# `commands/` (the shipped assets) and `specs/` (its own spec, tasks and pattern
|
|
64
|
+
# table). Demanding `templates/` and `scripts/` here asks about directories this
|
|
65
|
+
# script never opens.
|
|
66
|
+
#
|
|
67
|
+
# Measured before the split: the anti-coupling fixtures build exactly `commands/` and
|
|
68
|
+
# `specs/`, so every one of them was refused before the scan — and three unit tests went
|
|
69
|
+
# red while the real repository (which has all three) stayed green. A guard whose
|
|
70
|
+
# subjects can no longer be exercised is not a stronger guard.
|
|
71
|
+
is_scannable_base() { [ -d "$1/commands" ] && [ -d "$1/specs" ]; }
|
|
72
|
+
|
|
73
|
+
resolve_base() {
|
|
74
|
+
local cand here
|
|
75
|
+
if cand=$(git rev-parse --show-toplevel 2>/dev/null) && [ -n "$cand" ] && is_repo_root "$cand"; then
|
|
76
|
+
printf '%s' "$cand"; return 0
|
|
77
|
+
fi
|
|
78
|
+
here=$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
79
|
+
for cand in "$here/../.." "$here/../../.."; do
|
|
80
|
+
cand=$(CDPATH="" cd "$cand" 2>/dev/null && pwd) || continue
|
|
81
|
+
is_repo_root "$cand" && { printf '%s' "$cand"; return 0; }
|
|
82
|
+
done
|
|
83
|
+
return 1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if [ -n "${ANTI_COUPLING_BASE:-}" ]; then
|
|
87
|
+
BASE="$ANTI_COUPLING_BASE"
|
|
88
|
+
else
|
|
89
|
+
BASE=$(resolve_base) || BASE=''
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# ⚠️ The sentinel is chosen by the base's SOURCE, and the message names the one that ran —
|
|
93
|
+
# a reader told to expect `templates/` when the failing check never looks at it would go add
|
|
94
|
+
# a directory that changes nothing.
|
|
95
|
+
if [ -n "${ANTI_COUPLING_BASE:-}" ]; then
|
|
96
|
+
_sentinel_ok=0; is_scannable_base "$BASE" && _sentinel_ok=1
|
|
97
|
+
_expect="commands/ and specs/ (what this scanner reads)"
|
|
98
|
+
else
|
|
99
|
+
_sentinel_ok=0; is_repo_root "$BASE" && _sentinel_ok=1
|
|
100
|
+
_expect="commands/ , templates/ and scripts/ (a repository root)"
|
|
101
|
+
fi
|
|
102
|
+
if [ "$_sentinel_ok" -eq 0 ]; then
|
|
103
|
+
echo "✗ BASE is not usable: '${BASE:-<unresolved>}'" >&2
|
|
104
|
+
echo " Expected it to contain $_expect ." >&2
|
|
105
|
+
if [ -n "${ANTI_COUPLING_BASE:-}" ]; then
|
|
106
|
+
echo " Source: the ANTI_COUPLING_BASE override." >&2
|
|
107
|
+
else
|
|
108
|
+
echo " Source: auto-resolution (git toplevel, then the script's two possible depths)." >&2
|
|
109
|
+
fi
|
|
110
|
+
echo " ⚠️ Every finding below would be derived from a tree this script never read —" >&2
|
|
111
|
+
echo " an absent subject and a clean subject are the same output otherwise." >&2
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# The roots that SHIP. A user project receives these under different paths, but the
|
|
116
|
+
# class of file is the same; the caller may override with ANTI_COUPLING_ASSETS.
|
|
117
|
+
ASSETS="${ANTI_COUPLING_ASSETS:-commands templates scripts}"
|
|
118
|
+
PATTERNS="${ANTI_COUPLING_PATTERNS:-$BASE/specs/anti-coupling-patterns.txt}"
|
|
119
|
+
OWN_SPEC="${ANTI_COUPLING_OWN_SPEC:-$BASE/specs/spec.md}"
|
|
120
|
+
OWN_TASKS="${ANTI_COUPLING_OWN_TASKS:-$BASE/specs/tasks.md}"
|
|
121
|
+
|
|
122
|
+
FAIL=0
|
|
123
|
+
echo "Anti-coupling scan: $BASE"
|
|
124
|
+
echo
|
|
125
|
+
|
|
126
|
+
# --- class 1: foreign numeric identifiers (DERIVED, not listed) ----------------------
|
|
127
|
+
# ⚠️ **The criterion is O1's own wording: "A number above this document's own range is the
|
|
128
|
+
# mechanical tell."** It is a RANGE test, not a set difference — and the first run of this
|
|
129
|
+
# script is why that distinction matters. A set difference flagged `FR-5` · `FR-10` ·
|
|
130
|
+
# `US-1` out of `commands/specpro.checklist.md`, where they are ILLUSTRATIVE placeholders
|
|
131
|
+
# ("Spec §FR-5") that belong to no project at all. Those are not leakage, and reporting
|
|
132
|
+
# them as if they were teaches the reader to skim the output — after which a real hit
|
|
133
|
+
# reads the same as noise. O1 also says the *in-range* identifier carrying a host-specific
|
|
134
|
+
# gloss is "the one that needs a reader to notice": that half is deliberately NOT
|
|
135
|
+
# mechanised here, and saying so is the point.
|
|
136
|
+
echo " [1/3] foreign numeric identifiers (range-derived from $OWN_SPEC + tasks)"
|
|
137
|
+
|
|
138
|
+
# ⚠️ **Each prefix's range comes from the artifact that ALLOCATES it — not from every
|
|
139
|
+
# artifact that mentions it.** Reading the range from spec.md *and* tasks.md was wrong and
|
|
140
|
+
# the counter-example caught it: `tasks.md` quotes the foreign identifiers it must fix, so
|
|
141
|
+
# the derived maximum came out as the *quoted* value instead of the *allocated* one — and a
|
|
142
|
+
# range that has absorbed the very identifiers it should catch makes the whole class blind.
|
|
143
|
+
# The measurer must not be fed the thing being measured.
|
|
144
|
+
#
|
|
145
|
+
# ⚠️ **Written here as the prefix form, never as a real identifier** (FR-053): this file is
|
|
146
|
+
# a SHIPPED asset, so quoting the identifiers it exists to catch would ship one project's
|
|
147
|
+
# numbers to every user — the comment forbidding the leak would itself be an instance of it.
|
|
148
|
+
own_source() { # $1 = prefix → the artifact that allocates it
|
|
149
|
+
case "$1" in
|
|
150
|
+
FR | US) echo "$OWN_SPEC" ;;
|
|
151
|
+
T) echo "$OWN_TASKS" ;;
|
|
152
|
+
ISS) echo "${ANTI_COUPLING_OWN_LEDGER:-$BASE/specs/implement_issues.md}" ;;
|
|
153
|
+
D) echo "$OWN_SPEC" ;;
|
|
154
|
+
esac
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
own_max() { # $1 = prefix. The project's highest allocated number for it.
|
|
158
|
+
src=$(own_source "$1")
|
|
159
|
+
[ -f "$src" ] || return
|
|
160
|
+
grep -rhoE "\b$1-[0-9]+" "$src" 2>/dev/null | grep -oE '[0-9]+' | sort -n | tail -1
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
derived_any=0
|
|
164
|
+
foreign=""
|
|
165
|
+
for prefix in FR US T ISS D; do
|
|
166
|
+
max=$(own_max "$prefix")
|
|
167
|
+
if [ -z "$max" ]; then
|
|
168
|
+
# Not every prefix is used by every project; an unused one has nothing to
|
|
169
|
+
# compare against, which is a legitimate zero, not a silent gap.
|
|
170
|
+
continue
|
|
171
|
+
fi
|
|
172
|
+
derived_any=1
|
|
173
|
+
# ⚠️ `$BASE/$root`, never a bare `$ASSETS`: the bare form resolves against the CALLER's
|
|
174
|
+
# cwd, so it scanned whatever directory happened to be named `commands` — correct when
|
|
175
|
+
# run from the repository root, wrong everywhere else, and silently so. The fixture
|
|
176
|
+
# tree caught it; the real-repo run could not have.
|
|
177
|
+
# ⚠️ **`-o` WITHOUT `-h`, and the filename kept** (ISS-116 / T165). The `-h` this used to
|
|
178
|
+
# carry suppressed the file name, so a finding named the identifier and nothing else —
|
|
179
|
+
# the report line could name the identifier but not the `location`, while classes 2 and
|
|
180
|
+
# 3 both print the file. An O1 finding whose reader has to search the tree for it is a
|
|
181
|
+
# finding half-delivered.
|
|
182
|
+
# ⚠️ The identifier is written as the prefix form here too (FR-053) — same reason as
|
|
183
|
+
# `own_source` above: this is a shipped asset.
|
|
184
|
+
#
|
|
185
|
+
# ⚠️ The deduplication is on the WHOLE line, not on the identifier: the same foreign
|
|
186
|
+
# number legitimately appears in two files, and `sort -u` over the id alone would
|
|
187
|
+
# collapse them into one — hiding the second location. Sorting the composed lines
|
|
188
|
+
# dedupes identical (id, file) pairs and keeps distinct files distinct.
|
|
189
|
+
for root in $ASSETS; do
|
|
190
|
+
[ -d "$BASE/$root" ] || continue
|
|
191
|
+
while IFS= read -r hit; do
|
|
192
|
+
[ -n "$hit" ] || continue
|
|
193
|
+
file=${hit%%:*}
|
|
194
|
+
id=$(printf '%s' "$hit" | sed 's/^[^:]*://' | grep -oE "[A-Za-z]+-[0-9]+" | head -1)
|
|
195
|
+
n=$(printf '%s' "$id" | grep -oE '[0-9]+')
|
|
196
|
+
[ "$n" -gt "$max" ] 2>/dev/null && foreign="$foreign$id ($prefix max=$max) ${file#$BASE/}"$'\n'
|
|
197
|
+
done < <(grep -roE "\b$prefix-[0-9]+" "$BASE/$root" 2>/dev/null || true)
|
|
198
|
+
done
|
|
199
|
+
done
|
|
200
|
+
|
|
201
|
+
if [ "$derived_any" = 0 ]; then
|
|
202
|
+
echo " ✗ could not derive this project's own identifier ranges from $OWN_SPEC / $OWN_TASKS"
|
|
203
|
+
echo " → with no range to compare against, EVERY identifier reads as foreign (noise)"
|
|
204
|
+
echo " or none does (a silent pass). A scan that cannot tell its own numbers from"
|
|
205
|
+
echo " a foreign one MUST NOT report clean."
|
|
206
|
+
FAIL=1
|
|
207
|
+
elif [ -n "$foreign" ]; then
|
|
208
|
+
echo " ✗ identifiers above this project's own range, in shipped assets:"
|
|
209
|
+
# Sorted for the same reason class 2 is: the enumeration order is an implementation
|
|
210
|
+
# detail of the scanner, and the parity contract is "same stdout" (FR-008).
|
|
211
|
+
# ⚠️ `-u`, and it dedupes the COMPOSED line: the same id hit twice in one file collapses,
|
|
212
|
+
# while the same id in two files does not — the two lines differ by their file name.
|
|
213
|
+
printf '%s' "$foreign" | sed '/^$/d' | LC_ALL=C sort -u | sed 's/^/ /'
|
|
214
|
+
echo " → a reader outside this repository cannot resolve them (O1, HIGH)."
|
|
215
|
+
FAIL=1
|
|
216
|
+
else
|
|
217
|
+
echo " ✓ none"
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
# --- classes 2 and 3: declared patterns ---------------------------------------------
|
|
221
|
+
# ⚠️ The classes are spelled out here as the CLASS NAMES Pass O uses; the patterns are read
|
|
222
|
+
# from the project file. Both must be non-empty — "缺一即不通过".
|
|
223
|
+
check_class() {
|
|
224
|
+
local tag="$1" label="$2" severity="$3"
|
|
225
|
+
local n
|
|
226
|
+
n=$(grep -cE "^${tag}\|" "$PATTERNS" 2>/dev/null || true)
|
|
227
|
+
if [ "${n:-0}" -eq 0 ]; then
|
|
228
|
+
echo " ✗ no patterns declared for this class in $PATTERNS"
|
|
229
|
+
echo " → Pass O requires all three classes; an uncovered class is the finding,"
|
|
230
|
+
echo " not a gap to note and move past."
|
|
231
|
+
FAIL=1
|
|
232
|
+
return
|
|
233
|
+
fi
|
|
234
|
+
# ⚠️ **A hit is a FINDING unless the pattern file lists it as reviewed** (T200).
|
|
235
|
+
# The patterns are a coarse word filter; a match cannot tell a worked example from a
|
|
236
|
+
# parallel list of alternatives, and `T177` triaged this class's hits into both. The
|
|
237
|
+
# triage is recorded in the pattern file as `reviewed::<class>::<regex>::<file>::<why>`,
|
|
238
|
+
# so the classification is an INPUT the reviewer can see and change — never a silent
|
|
239
|
+
# relaxation. Removing an entry restores its finding.
|
|
240
|
+
local hits="" reviewed_hits=""
|
|
241
|
+
while IFS='|' read -r _ regex why; do
|
|
242
|
+
[ -z "${regex:-}" ] && continue
|
|
243
|
+
for root in $ASSETS; do
|
|
244
|
+
[ -d "$BASE/$root" ] || continue
|
|
245
|
+
while IFS= read -r f; do
|
|
246
|
+
[ -n "$f" ] || continue
|
|
247
|
+
rel="${f#"$BASE"/}"
|
|
248
|
+
if grep -qF "reviewed::${tag}::${regex}::${rel}::" "$PATTERNS" 2>/dev/null; then
|
|
249
|
+
reviewed_hits="$reviewed_hits ($why) $rel"$'\n'
|
|
250
|
+
else
|
|
251
|
+
hits="$hits ($why) $rel"$'\n'
|
|
252
|
+
fi
|
|
253
|
+
done < <(grep -rlE "$regex" "$BASE/$root" 2>/dev/null || true)
|
|
254
|
+
done
|
|
255
|
+
done < <(grep -E "^${tag}\|" "$PATTERNS")
|
|
256
|
+
if [ -n "$reviewed_hits" ]; then
|
|
257
|
+
echo " ⊘ reviewed generic reference(s) — NOT findings, each with its reason in"
|
|
258
|
+
echo " $PATTERNS (remove an entry to restore it as a finding):"
|
|
259
|
+
printf '%s' "$reviewed_hits" | sed '/^$/d' | LC_ALL=C sort | sed 's/^/ /'
|
|
260
|
+
fi
|
|
261
|
+
if [ -n "$hits" ]; then
|
|
262
|
+
echo " ✗ $label present in shipped assets ($severity):"
|
|
263
|
+
# ⚠️ Sorted, because the enumeration order is an implementation detail of the
|
|
264
|
+
# scanner (grep here, Get-ChildItem in the twin) and the parity contract is
|
|
265
|
+
# "same stdout". A correct set in a different order is still a different stdout.
|
|
266
|
+
printf '%s' "$hits" | sed '/^$/d' | LC_ALL=C sort | sed 's/^/ /'
|
|
267
|
+
FAIL=1
|
|
268
|
+
else
|
|
269
|
+
# ⚠️ Reported even when the ⊘ block above is present: "no findings" and "nothing was
|
|
270
|
+
# scanned" must not look alike, and the reviewed set is disclosed rather than hidden.
|
|
271
|
+
echo " ✓ none"
|
|
272
|
+
fi
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
echo " [2/3] host technology stack (patterns from $PATTERNS)"
|
|
276
|
+
check_class "tech" "host technology stack" "O2, HIGH"
|
|
277
|
+
|
|
278
|
+
echo " [3/3] host business terms and module names (patterns from $PATTERNS)"
|
|
279
|
+
check_class "term" "host business term / module name" "O3, HIGH"
|
|
280
|
+
|
|
281
|
+
# --- verdict --------------------------------------------------------------------------
|
|
282
|
+
echo
|
|
283
|
+
if [ "$FAIL" = 0 ]; then
|
|
284
|
+
echo "✓ anti-coupling: no findings across all three classes"
|
|
285
|
+
exit 0
|
|
286
|
+
else
|
|
287
|
+
echo "✗ anti-coupling: FINDINGS (see above)"
|
|
288
|
+
echo " → Pass O reports only; the fix is a tool-source change. Register it in the"
|
|
289
|
+
echo " [tasks] section of specs/implement_issues.md — a finding without a route is"
|
|
290
|
+
echo " indistinguishable from one never reported. And the fix has TWO halves: remove"
|
|
291
|
+
echo " the trace, AND add the pattern that would have caught it."
|
|
292
|
+
exit 1
|
|
293
|
+
fi
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Consolidated prerequisite checking script
|
|
4
|
+
#
|
|
5
|
+
# This script provides unified prerequisite checking for Spec-Driven Development workflow.
|
|
6
|
+
# It replaces the functionality previously spread across multiple scripts.
|
|
7
|
+
#
|
|
8
|
+
# Usage: ./check-prerequisites.sh [OPTIONS]
|
|
9
|
+
#
|
|
10
|
+
# OPTIONS:
|
|
11
|
+
# --json Output in JSON format
|
|
12
|
+
# --require-tasks Require tasks.md to exist (for implementation phase)
|
|
13
|
+
# --include-tasks Include tasks.md in AVAILABLE_DOCS list
|
|
14
|
+
# --paths-only Only output path variables (no validation)
|
|
15
|
+
# --help, -h Show help message
|
|
16
|
+
#
|
|
17
|
+
# OUTPUTS:
|
|
18
|
+
# JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]}
|
|
19
|
+
# Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md
|
|
20
|
+
# Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc.
|
|
21
|
+
|
|
22
|
+
set -e
|
|
23
|
+
|
|
24
|
+
# Parse command line arguments
|
|
25
|
+
JSON_MODE=false
|
|
26
|
+
REQUIRE_TASKS=false
|
|
27
|
+
INCLUDE_TASKS=false
|
|
28
|
+
PATHS_ONLY=false
|
|
29
|
+
|
|
30
|
+
for arg in "$@"; do
|
|
31
|
+
case "$arg" in
|
|
32
|
+
--json)
|
|
33
|
+
JSON_MODE=true
|
|
34
|
+
;;
|
|
35
|
+
--require-tasks)
|
|
36
|
+
REQUIRE_TASKS=true
|
|
37
|
+
;;
|
|
38
|
+
--include-tasks)
|
|
39
|
+
INCLUDE_TASKS=true
|
|
40
|
+
;;
|
|
41
|
+
--paths-only)
|
|
42
|
+
PATHS_ONLY=true
|
|
43
|
+
;;
|
|
44
|
+
--help|-h)
|
|
45
|
+
cat << 'EOF'
|
|
46
|
+
Usage: check-prerequisites.sh [OPTIONS]
|
|
47
|
+
|
|
48
|
+
Consolidated prerequisite checking for Spec-Driven Development workflow.
|
|
49
|
+
|
|
50
|
+
OPTIONS:
|
|
51
|
+
--json Output in JSON format
|
|
52
|
+
--require-tasks Require tasks.md to exist (for implementation phase)
|
|
53
|
+
--include-tasks Include tasks.md in AVAILABLE_DOCS list
|
|
54
|
+
--paths-only Only output path variables (no prerequisite validation)
|
|
55
|
+
--help, -h Show this help message
|
|
56
|
+
|
|
57
|
+
EXAMPLES:
|
|
58
|
+
# Check task prerequisites (plan.md required)
|
|
59
|
+
./check-prerequisites.sh --json
|
|
60
|
+
|
|
61
|
+
# Check implementation prerequisites (plan.md + tasks.md required)
|
|
62
|
+
./check-prerequisites.sh --json --require-tasks --include-tasks
|
|
63
|
+
|
|
64
|
+
# Get feature paths only (no validation)
|
|
65
|
+
./check-prerequisites.sh --paths-only
|
|
66
|
+
|
|
67
|
+
EOF
|
|
68
|
+
exit 0
|
|
69
|
+
;;
|
|
70
|
+
*)
|
|
71
|
+
echo "ERROR: Unknown option '$arg'. Use --help for usage information." >&2
|
|
72
|
+
exit 1
|
|
73
|
+
;;
|
|
74
|
+
esac
|
|
75
|
+
done
|
|
76
|
+
|
|
77
|
+
# Source common functions
|
|
78
|
+
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
79
|
+
source "$SCRIPT_DIR/common.sh"
|
|
80
|
+
|
|
81
|
+
# Get feature paths
|
|
82
|
+
eval $(get_feature_paths)
|
|
83
|
+
|
|
84
|
+
# If paths-only mode, output paths and exit (support JSON + paths-only combined)
|
|
85
|
+
if $PATHS_ONLY; then
|
|
86
|
+
if $JSON_MODE; then
|
|
87
|
+
# Minimal JSON paths payload (no validation performed)
|
|
88
|
+
printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_SCOPE":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
|
|
89
|
+
"$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_SCOPE" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS"
|
|
90
|
+
else
|
|
91
|
+
echo "REPO_ROOT: $REPO_ROOT"
|
|
92
|
+
echo "BRANCH: $CURRENT_BRANCH"
|
|
93
|
+
echo "FEATURE_SCOPE: $FEATURE_SCOPE"
|
|
94
|
+
echo "FEATURE_DIR: $FEATURE_DIR"
|
|
95
|
+
echo "FEATURE_SPEC: $FEATURE_SPEC"
|
|
96
|
+
echo "IMPL_PLAN: $IMPL_PLAN"
|
|
97
|
+
echo "TASKS: $TASKS"
|
|
98
|
+
fi
|
|
99
|
+
exit 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# Validate required directories and files
|
|
103
|
+
if [[ ! -d "$FEATURE_DIR" ]]; then
|
|
104
|
+
echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2
|
|
105
|
+
echo "Run /specpro-specify first to create the feature structure." >&2
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
if [[ ! -f "$IMPL_PLAN" ]]; then
|
|
110
|
+
echo "ERROR: plan.md not found in $FEATURE_DIR" >&2
|
|
111
|
+
echo "Run /specpro-plan first to create the implementation plan." >&2
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# Check for tasks.md if required
|
|
116
|
+
if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then
|
|
117
|
+
echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2
|
|
118
|
+
echo "Run /specpro-tasks first to create the task list." >&2
|
|
119
|
+
exit 1
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# Build list of available documents
|
|
123
|
+
docs=()
|
|
124
|
+
|
|
125
|
+
# CRITICAL: spec.md is the primary specification document
|
|
126
|
+
# Created by /specpro-specify, read by all subsequent commands
|
|
127
|
+
[[ -f "$FEATURE_SPEC" ]] && docs+=("spec.md")
|
|
128
|
+
|
|
129
|
+
# Always check these optional docs
|
|
130
|
+
[[ -f "$RESEARCH" ]] && docs+=("research.md")
|
|
131
|
+
[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")
|
|
132
|
+
|
|
133
|
+
# Check contracts directory (only if it exists and has files)
|
|
134
|
+
if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then
|
|
135
|
+
docs+=("contracts/")
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")
|
|
139
|
+
|
|
140
|
+
# Include tasks.md if requested and it exists
|
|
141
|
+
if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then
|
|
142
|
+
docs+=("tasks.md")
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# Include test-tasks.md if it exists (optional high-level test plan; never required)
|
|
146
|
+
[[ -f "$TEST_TASKS" ]] && docs+=("test-tasks.md")
|
|
147
|
+
|
|
148
|
+
# Output results
|
|
149
|
+
if $JSON_MODE; then
|
|
150
|
+
# Build JSON array of documents
|
|
151
|
+
if [[ ${#docs[@]} -eq 0 ]]; then
|
|
152
|
+
json_docs="[]"
|
|
153
|
+
else
|
|
154
|
+
json_docs=$(printf '"%s",' "${docs[@]}")
|
|
155
|
+
json_docs="[${json_docs%,}]"
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
printf '{"FEATURE_DIR":"%s","FEATURE_SCOPE":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$FEATURE_SCOPE" "$json_docs"
|
|
159
|
+
else
|
|
160
|
+
# Text output
|
|
161
|
+
echo "FEATURE_DIR:$FEATURE_DIR"
|
|
162
|
+
echo "AVAILABLE_DOCS:"
|
|
163
|
+
|
|
164
|
+
# Show status of each potential document
|
|
165
|
+
check_file "$FEATURE_SPEC" "spec.md"
|
|
166
|
+
check_file "$RESEARCH" "research.md"
|
|
167
|
+
check_file "$DATA_MODEL" "data-model.md"
|
|
168
|
+
check_dir "$CONTRACTS_DIR" "contracts/"
|
|
169
|
+
check_file "$QUICKSTART" "quickstart.md"
|
|
170
|
+
|
|
171
|
+
if $INCLUDE_TASKS; then
|
|
172
|
+
check_file "$TASKS" "tasks.md"
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
check_file "$TEST_TASKS" "test-tasks.md"
|
|
176
|
+
fi
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Common functions and variables for all scripts
|
|
3
|
+
|
|
4
|
+
# Get repository root, with fallback for non-git repositories
|
|
5
|
+
get_repo_root() {
|
|
6
|
+
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
|
7
|
+
git rev-parse --show-toplevel
|
|
8
|
+
else
|
|
9
|
+
# Fall back to script location for non-git repos
|
|
10
|
+
local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
(cd "$script_dir/../../.." && pwd)
|
|
12
|
+
fi
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Get current branch (for informational purposes only)
|
|
16
|
+
# NOTE: Branch information is NOT used for directory structure
|
|
17
|
+
# All spec files are stored directly in specs/ directory
|
|
18
|
+
get_current_branch() {
|
|
19
|
+
# Check git if available
|
|
20
|
+
if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then
|
|
21
|
+
git rev-parse --abbrev-ref HEAD
|
|
22
|
+
return
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
echo "unknown" # Fallback for non-git repos
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Check if we have git available
|
|
29
|
+
has_git() {
|
|
30
|
+
git rev-parse --show-toplevel >/dev/null 2>&1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Get feature paths - all files are in specs/ root directory
|
|
34
|
+
get_feature_paths() {
|
|
35
|
+
local repo_root=$(get_repo_root)
|
|
36
|
+
local current_branch=$(get_current_branch)
|
|
37
|
+
local has_git_repo="false"
|
|
38
|
+
|
|
39
|
+
if has_git; then
|
|
40
|
+
has_git_repo="true"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# All spec files are in specs/ root directory (no branch subdirectories)
|
|
44
|
+
local specs_dir="$repo_root/specs"
|
|
45
|
+
|
|
46
|
+
# ── Feature-scope detection (FR-063 / T050, v0.23) ──
|
|
47
|
+
# cwd inside specs/fNNN-简称/ ⇒ feature scope: spec/plan/tasks/test-tasks
|
|
48
|
+
# resolve to THAT directory; constitution/research stay mother-side
|
|
49
|
+
# (living-reference inheritance, FR-014). Greppable anchors: 「feature 作用域」/「母作用域」
|
|
50
|
+
local scope_dir="$(pwd -P)"
|
|
51
|
+
local in_feature="false"
|
|
52
|
+
case "$scope_dir" in
|
|
53
|
+
*/specs/f[0-9][0-9][0-9]-*) in_feature="true" ;;
|
|
54
|
+
esac
|
|
55
|
+
|
|
56
|
+
local fd implan tasks tt
|
|
57
|
+
if [ "$in_feature" = "true" ]; then
|
|
58
|
+
fd="$scope_dir"
|
|
59
|
+
implan="$scope_dir/plan.md"
|
|
60
|
+
tasks="$scope_dir/tasks.md"
|
|
61
|
+
tt="$scope_dir/test-tasks.md"
|
|
62
|
+
else
|
|
63
|
+
fd="$specs_dir"
|
|
64
|
+
implan="$specs_dir/plan.md"
|
|
65
|
+
tasks="$specs_dir/tasks.md"
|
|
66
|
+
tt="$specs_dir/test-tasks.md"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# FEATURE_SCOPE: 'true' = feature 作用域 / 'false' = 母作用域 (FR-063)
|
|
70
|
+
cat <<EOF
|
|
71
|
+
FEATURE_SCOPE='$in_feature'
|
|
72
|
+
REPO_ROOT='$repo_root'
|
|
73
|
+
CURRENT_BRANCH='$current_branch'
|
|
74
|
+
HAS_GIT='$has_git_repo'
|
|
75
|
+
FEATURE_DIR='$fd'
|
|
76
|
+
FEATURE_SPEC='$fd/spec.md'
|
|
77
|
+
IMPL_PLAN='$implan'
|
|
78
|
+
TASKS='$tasks'
|
|
79
|
+
RESEARCH='$specs_dir/research.md'
|
|
80
|
+
DATA_MODEL='$specs_dir/data-model.md'
|
|
81
|
+
QUICKSTART='$specs_dir/quickstart.md'
|
|
82
|
+
TEST_TASKS='$tt'
|
|
83
|
+
CONTRACTS_DIR='$fd/contracts'
|
|
84
|
+
EOF
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
|
88
|
+
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
|