specpro-cli 0.1.0__py3-none-any.whl

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