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,376 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Ledger Integrity Verification Script
|
|
4
|
+
# Verifies specs/implement_issues.md structural invariants.
|
|
5
|
+
#
|
|
6
|
+
# Why this exists (2026-09-12): the ledger is a POSITION-STRUCTURED file —
|
|
7
|
+
# an entry's section is decided purely by where it sits in the file. Every
|
|
8
|
+
# writer that "appends" therefore silently depends on which section happens
|
|
9
|
+
# to be last, and that fact changes as sections are added. Observed failure:
|
|
10
|
+
# ISS-049 (a [tasks]-side finding) was appended to the END OF THE FILE and
|
|
11
|
+
# landed in [test-plan], whose consumer has no mandate to act on it — the
|
|
12
|
+
# entry looked registered, the statistics table looked plausible, nothing
|
|
13
|
+
# errored. Two statistics drifts were found in the same review, also silent.
|
|
14
|
+
#
|
|
15
|
+
# Invariants checked:
|
|
16
|
+
# 1. The four phase sections exist, in canonical order
|
|
17
|
+
# 2. Every issue entry belongs to one of those sections (nothing orphaned
|
|
18
|
+
# outside a section — the "appended past the sentinel" signature)
|
|
19
|
+
# 3. The statistics table row for each section matches its actual entry count
|
|
20
|
+
# 4. Pending-Items names only entries that exist as pending entries
|
|
21
|
+
# 5. The file ends with exactly one newline (append-based writers rely on it;
|
|
22
|
+
# a missing trailing newline fuses the next entry onto the previous line)
|
|
23
|
+
# 6. Every entry block carries exactly ONE Related line, and every field line
|
|
24
|
+
# lies INSIDE some entry block. A block runs to the nearest of: the next
|
|
25
|
+
# entry, the next section heading, the sentinel. Both directions matter —
|
|
26
|
+
# a tail split off into the next block and a tail written past a boundary
|
|
27
|
+
# are the same defect seen from two sides.
|
|
28
|
+
#
|
|
29
|
+
# Usage: .specpro/scripts/bash/verify-ledger.sh
|
|
30
|
+
# LEDGER=<path> .specpro/scripts/bash/verify-ledger.sh (verify a copy — used by its own tests)
|
|
31
|
+
# Exit: 0 = all invariants hold; 1 = at least one violation (details printed)
|
|
32
|
+
|
|
33
|
+
set -uo pipefail
|
|
34
|
+
|
|
35
|
+
LEDGER="${LEDGER:-specs/implement_issues.md}"
|
|
36
|
+
# The contract is resolved RELATIVE TO THE LEDGER, not to a fixed path: in the
|
|
37
|
+
# two-repository layout this script runs from the root repo (ledger at
|
|
38
|
+
# specs/implement_issues.md) AND from inside the specs repo (ledger at
|
|
39
|
+
# implement_issues.md), and a hardcoded specs/contracts/... resolves to nothing
|
|
40
|
+
# in the second case — where the verifier then refuses to run at all. The hook
|
|
41
|
+
# caught exactly that on the first commit after this change.
|
|
42
|
+
CONTRACT="${CONTRACT:-$(dirname "$LEDGER")/contracts/ledger.md}"
|
|
43
|
+
|
|
44
|
+
# --- 0. the section list is DERIVED, not transcribed -------------------------
|
|
45
|
+
# `## Sections and their consumers` in the contract is the single source for which
|
|
46
|
+
# sections exist, in what order. This array used to be a hand-copied duplicate of
|
|
47
|
+
# that table, and the two diverged exactly the way two sources of one fact do: the
|
|
48
|
+
# contract gained a row for a section the ledger did not have yet, and **nothing
|
|
49
|
+
# reported it** — invariant #1 was violated for as long as that lasted. A copy is
|
|
50
|
+
# not a check; deriving one from the other is.
|
|
51
|
+
SECTIONS=()
|
|
52
|
+
if [ -f "$CONTRACT" ]; then
|
|
53
|
+
while IFS= read -r s; do
|
|
54
|
+
# Table rows look like: | `<name>` | <consumer> | <what it does> |
|
|
55
|
+
[ -n "$s" ] && SECTIONS+=("$s")
|
|
56
|
+
done < <(sed -n '/^## Sections and their consumers/,/^## /p' "$CONTRACT" \
|
|
57
|
+
| sed -n 's/^| `\[\([a-z-]*\)\]`.*/\1/p')
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if [ "${#SECTIONS[@]}" -eq 0 ]; then
|
|
61
|
+
# ⚠️ An empty list is NOT "nothing to check": every section-level check below
|
|
62
|
+
# would then pass vacuously, and that is indistinguishable from a clean ledger.
|
|
63
|
+
echo "✗ could not derive the section list from $CONTRACT" >&2
|
|
64
|
+
echo " A verifier with no sections to check reports success without checking anything." >&2
|
|
65
|
+
echo " Fix: run from the repository root, or set CONTRACT=<path to the ledger contract>." >&2
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
FAIL=0
|
|
70
|
+
|
|
71
|
+
# --- 0b. and the comparison runs BOTH ways -----------------------------------
|
|
72
|
+
# Deriving the list closes "the contract names a section the ledger lacks". It does
|
|
73
|
+
# NOT close the reverse: a section present in the ledger but absent from the contract
|
|
74
|
+
# is simply never iterated, so it is never checked — and the run still reports clean.
|
|
75
|
+
# That is the same silent-uncoverage the derived list was meant to remove, one
|
|
76
|
+
# direction over.
|
|
77
|
+
if [ -f "$LEDGER" ]; then
|
|
78
|
+
in_ledger=$(grep -oE '^## \[[a-z-]+\] Phase Issues$' "$LEDGER" | sed 's/^## \[\([a-z-]*\)\].*/\1/' | sort -u)
|
|
79
|
+
in_contract=$(printf '%s\n' "${SECTIONS[@]}" | sort -u)
|
|
80
|
+
undeclared=$(comm -23 <(printf '%s\n' "$in_ledger") <(printf '%s\n' "$in_contract"))
|
|
81
|
+
if [ -n "$undeclared" ]; then
|
|
82
|
+
echo "✗ section(s) present in the ledger but absent from the contract's table:" >&2
|
|
83
|
+
printf ' %s\n' $undeclared >&2
|
|
84
|
+
echo " → nothing checks them: the verifier iterates the contract's list." >&2
|
|
85
|
+
echo " Add the row to $CONTRACT (with its consumer), or remove the section." >&2
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
if [ ! -f "$LEDGER" ]; then
|
|
91
|
+
echo "✗ $LEDGER not found (run from the repository root)" >&2
|
|
92
|
+
exit 1
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
echo "Ledger integrity: $LEDGER"
|
|
96
|
+
echo
|
|
97
|
+
|
|
98
|
+
# --- 1. sections exist, in canonical order -----------------------------------
|
|
99
|
+
seen=""
|
|
100
|
+
order_ok=1
|
|
101
|
+
for s in "${SECTIONS[@]}"; do
|
|
102
|
+
line=$(grep -n "^## \[$s\] Phase Issues$" "$LEDGER" | head -1 | cut -d: -f1)
|
|
103
|
+
if [ -z "$line" ]; then
|
|
104
|
+
echo "✗ missing section: ## [$s] Phase Issues"
|
|
105
|
+
FAIL=1; order_ok=0
|
|
106
|
+
else
|
|
107
|
+
seen="$seen $line"
|
|
108
|
+
printf " ✓ [%-9s] heading at line %s\n" "$s" "$line"
|
|
109
|
+
fi
|
|
110
|
+
done
|
|
111
|
+
if [ "$order_ok" = 1 ]; then
|
|
112
|
+
sorted=$(echo $seen | tr ' ' '\n' | sort -n | tr '\n' ' ')
|
|
113
|
+
[ "$(echo $seen)" = "${sorted% }" ] || { echo "✗ sections are not in canonical order"; FAIL=1; }
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# --- 2. no orphaned entries (entry outside every section) --------------------
|
|
117
|
+
# An entry is orphaned when it appears after the last section's heading but
|
|
118
|
+
# outside it — in practice: appended after the end-of-file sentinel.
|
|
119
|
+
last_heading=$(grep -n "^## \[.*\] Phase Issues$" "$LEDGER" | tail -1 | cut -d: -f1)
|
|
120
|
+
sentinel=$(grep -n "^-->$" "$LEDGER" | tail -1 | cut -d: -f1)
|
|
121
|
+
if [ -n "$sentinel" ] && [ -n "$last_heading" ] && [ "$sentinel" -gt "$last_heading" ]; then
|
|
122
|
+
orphan=$(awk -v s="$sentinel" 'NR>s && /^- \[[ x]\] ISS-/' "$LEDGER")
|
|
123
|
+
if [ -n "$orphan" ]; then
|
|
124
|
+
echo "✗ orphaned entry after the end-of-file sentinel (line $sentinel):"
|
|
125
|
+
echo "$orphan" | cut -c1-90 | sed 's/^/ /'
|
|
126
|
+
echo " → it belongs to no section; move it into its own section"
|
|
127
|
+
FAIL=1
|
|
128
|
+
else
|
|
129
|
+
echo " ✓ no entries appended past the end-of-file sentinel"
|
|
130
|
+
fi
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# --- 3. statistics table matches actual counts -------------------------------
|
|
134
|
+
echo
|
|
135
|
+
for s in "${SECTIONS[@]}"; do
|
|
136
|
+
h=$(grep -n "^## \[$s\] Phase Issues$" "$LEDGER" | head -1 | cut -d: -f1)
|
|
137
|
+
[ -z "$h" ] && continue
|
|
138
|
+
# Section body runs until the next '## [' heading, the end-of-file sentinel,
|
|
139
|
+
# or EOF — whichever comes first. Stopping at the sentinel matters: content
|
|
140
|
+
# appended past it is orphaned and must NOT be counted as part of the last
|
|
141
|
+
# section (otherwise one misfiling is reported twice, as an orphan AND as a
|
|
142
|
+
# count mismatch, and the count mismatch points at the wrong section).
|
|
143
|
+
next=$(awk -v s="$h" 'NR>s && (/^## \[/ || /^-->$/){print NR; exit}' "$LEDGER")
|
|
144
|
+
if [ -n "$next" ]; then
|
|
145
|
+
body=$(awk -v a="$h" -v b="$next" 'NR>a && NR<b' "$LEDGER")
|
|
146
|
+
else
|
|
147
|
+
body=$(awk -v a="$h" 'NR>a' "$LEDGER")
|
|
148
|
+
fi
|
|
149
|
+
res=$(echo "$body" | grep -c "^- \[x\] ISS-")
|
|
150
|
+
pen=$(echo "$body" | grep -c "^- \[ \] ISS-")
|
|
151
|
+
tot=$((res + pen))
|
|
152
|
+
row=$(grep "^| \[$s\]" "$LEDGER" | head -1)
|
|
153
|
+
t_tot=$(echo "$row" | awk -F'|' '{gsub(/ /,"",$3); print $3}')
|
|
154
|
+
t_res=$(echo "$row" | awk -F'|' '{gsub(/ /,"",$4); print $4}')
|
|
155
|
+
t_pen=$(echo "$row" | awk -F'|' '{gsub(/ /,"",$5); print $5}')
|
|
156
|
+
if [ "$tot" = "$t_tot" ] && [ "$res" = "$t_res" ] && [ "$pen" = "$t_pen" ]; then
|
|
157
|
+
printf " ✓ [%-9s] actual %s/%s/%s = table\n" "$s" "$tot" "$res" "$pen"
|
|
158
|
+
else
|
|
159
|
+
printf " ✗ [%-9s] actual %s/%s/%s ≠ table %s/%s/%s\n" "$s" "$tot" "$res" "$pen" "$t_tot" "$t_res" "$t_pen"
|
|
160
|
+
# Direction matters — the two directions have different root causes and
|
|
161
|
+
# different correct fixes. Telling the reader to "fix the table" is the
|
|
162
|
+
# wrong advice for the over-count direction (observed 2026-09-13): there
|
|
163
|
+
# the entry was never written, and the fix is to write it.
|
|
164
|
+
if [ "$t_tot" -gt "$tot" ] 2>/dev/null; then
|
|
165
|
+
echo " → table EXCEEDS the section by $((t_tot - tot)): the summary claims entries that do not exist."
|
|
166
|
+
echo " Signature of a SKIPPED WRITE — most often an idempotency guard that searched"
|
|
167
|
+
echo " the whole file for the ID and matched a mention (the statistics Pending-Items"
|
|
168
|
+
echo " column, the Last Updated line, a cross-reference) instead of the entry itself."
|
|
169
|
+
echo " Fix: write the missing entry body (and confirm the guard matches '^- \\[[x ]\\] ISS-<N>:' )."
|
|
170
|
+
elif [ "$t_tot" -lt "$tot" ] 2>/dev/null; then
|
|
171
|
+
echo " → section EXCEEDS the table by $((tot - t_tot)): the derived summary was not updated."
|
|
172
|
+
echo " Fix: update the table (the section entries are authoritative)."
|
|
173
|
+
else
|
|
174
|
+
echo " → counts agree but the resolved/pending split differs; reconcile against the section."
|
|
175
|
+
fi
|
|
176
|
+
FAIL=1
|
|
177
|
+
fi
|
|
178
|
+
done
|
|
179
|
+
|
|
180
|
+
# --- 4. Pending-Items column names only entries that exist -------------------
|
|
181
|
+
# ID-level check, independent of the counts: every ID listed in a section's
|
|
182
|
+
# Pending-Items column must exist as a pending entry in THAT section. This is
|
|
183
|
+
# the direct signature of the skipped-write defect — a summary naming an entry
|
|
184
|
+
# that was never written. Counts can coincide (one skipped + one un-bumped
|
|
185
|
+
# elsewhere) while this still fails, so it is not redundant with check 3.
|
|
186
|
+
echo
|
|
187
|
+
for s in "${SECTIONS[@]}"; do
|
|
188
|
+
h=$(grep -n "^## \[$s\] Phase Issues$" "$LEDGER" | head -1 | cut -d: -f1)
|
|
189
|
+
[ -z "$h" ] && continue
|
|
190
|
+
next=$(awk -v s="$h" 'NR>s && (/^## \[/ || /^-->$/){print NR; exit}' "$LEDGER")
|
|
191
|
+
if [ -n "$next" ]; then
|
|
192
|
+
body=$(awk -v a="$h" -v b="$next" 'NR>a && NR<b' "$LEDGER")
|
|
193
|
+
else
|
|
194
|
+
body=$(awk -v a="$h" 'NR>a' "$LEDGER")
|
|
195
|
+
fi
|
|
196
|
+
named=$(grep "^| \[$s\]" "$LEDGER" | head -1 | awk -F'|' '{print $6}' | grep -oE 'ISS-[0-9]+' | sort -u)
|
|
197
|
+
if [ -z "$named" ]; then
|
|
198
|
+
printf " ✓ [%-9s] Pending-Items column names no IDs\n" "$s"
|
|
199
|
+
continue
|
|
200
|
+
fi
|
|
201
|
+
miss=""
|
|
202
|
+
for id in $named; do
|
|
203
|
+
# A herestring, not a pipe. Under `pipefail`, `echo "$body" | grep -q …` can
|
|
204
|
+
# return 141: grep exits early on a match, echo takes SIGPIPE writing the rest,
|
|
205
|
+
# and the pipeline's status then reports failure. An entry that IS present reads
|
|
206
|
+
# as missing — and only sometimes, because the race is decided by scheduling.
|
|
207
|
+
# A verdict that is not a function of the input is not a verdict.
|
|
208
|
+
grep -qE "^- \[ \] ${id}:" <<<"$body" || miss="$miss $id"
|
|
209
|
+
done
|
|
210
|
+
if [ -z "$miss" ]; then
|
|
211
|
+
printf " ✓ [%-9s] every named pending ID exists as an entry\n" "$s"
|
|
212
|
+
else
|
|
213
|
+
printf " ✗ [%-9s] Pending-Items names %s but no such pending entry exists in this section\n" "$s" "$miss"
|
|
214
|
+
echo " → the summary names an entry the file does not contain — a skipped or lost write,"
|
|
215
|
+
echo " not a formatting slip. Write the entry body, or correct the column if it was over-bumped."
|
|
216
|
+
FAIL=1
|
|
217
|
+
fi
|
|
218
|
+
done
|
|
219
|
+
|
|
220
|
+
# --- 4b. a completed hand-off names an entry that EXISTS in its target section --------
|
|
221
|
+
# ISS-110 / ISS-93 / ISS-90 ③: a decision whose closing line was "hand this to
|
|
222
|
+
# /specpro-tasks --review-issues" created no `[tasks]` entry — so that command read an
|
|
223
|
+
# EMPTY queue and exited having done nothing, while the decision read as complete. The
|
|
224
|
+
# channel was never the problem (登记即路由 — any command may append to any section);
|
|
225
|
+
# what was missing was the OBLIGATION to write the entry, and a point where its absence
|
|
226
|
+
# stops something. This is that point.
|
|
227
|
+
#
|
|
228
|
+
# ⚠️ **The anchor is a LINE-START form, never a full-text match** (TOOL-009). The ledger is
|
|
229
|
+
# full of prose ABOUT hand-offs — this very comment is an example — and a check that
|
|
230
|
+
# matched a mention would fire on the discussion instead of the act. The form is fixed by
|
|
231
|
+
# the rule in `commands/specpro.plan.md`, and it is fixed precisely so this can be judged
|
|
232
|
+
# mechanically: an intention scattered through prose has no shape to test.
|
|
233
|
+
#
|
|
234
|
+
# ⚠️ **Only `[x]` entries are checked.** A `[ ]` entry has not handed anything off yet;
|
|
235
|
+
# demanding its target exist would punish the correct order of work.
|
|
236
|
+
#
|
|
237
|
+
# ⚠️ The arrow is the literal `→`. It is part of the anchor, not decoration: a rule that
|
|
238
|
+
# can be written three ways cannot be found by one pattern.
|
|
239
|
+
echo
|
|
240
|
+
handoffs=$(awk '
|
|
241
|
+
/^- \[[ x]\] ISS-[0-9]+:/ { cur = $0 }
|
|
242
|
+
/^ \*\*移交\*\*: \[[a-z-]+\] → ISS-[0-9]+/ { if (cur ~ /^- \[x\]/) print NR "\t" cur "\t" $0 }
|
|
243
|
+
' "$LEDGER")
|
|
244
|
+
|
|
245
|
+
if [ -z "$handoffs" ]; then
|
|
246
|
+
echo " ✓ no completed entry carries a hand-off line (nothing to resolve)"
|
|
247
|
+
else
|
|
248
|
+
bad=0
|
|
249
|
+
while IFS="$(printf '\t')" read -r lineno entry_line handoff_line; do
|
|
250
|
+
target=$(printf '%s' "$handoff_line" | sed -E 's/.*\[([a-z-]+)\].*/\1/')
|
|
251
|
+
hid=$(printf '%s' "$handoff_line" | grep -oE 'ISS-[0-9]+' | head -1)
|
|
252
|
+
src=$(printf '%s' "$entry_line" | grep -oE 'ISS-[0-9]+' | head -1)
|
|
253
|
+
|
|
254
|
+
# (a) the target must be a partition the contract actually defines. A hand-off to
|
|
255
|
+
# a partition nobody consumes is a dead end wearing a route's clothes (Q2).
|
|
256
|
+
if ! printf '%s\n' "${SECTIONS[@]}" | grep -qx "$target"; then
|
|
257
|
+
printf " ✗ hand-off at line %s targets [%s], which the contract does not define\n" "$lineno" "$target"
|
|
258
|
+
echo " → the named partition has no consumer; a hand-off there is a dead end."
|
|
259
|
+
bad=1
|
|
260
|
+
continue
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
# (b) the named entry must exist IN THAT SECTION. Not "somewhere in the file" —
|
|
264
|
+
# the whole point of a section is that a specific consumer reads it.
|
|
265
|
+
h=$(grep -n "^## \[$target\] Phase Issues$" "$LEDGER" | head -1 | cut -d: -f1)
|
|
266
|
+
next=$(awk -v s="$h" 'NR>s && (/^## \[/ || /^-->$/){print NR; exit}' "$LEDGER")
|
|
267
|
+
if [ -n "$next" ]; then
|
|
268
|
+
body=$(awk -v a="$h" -v b="$next" 'NR>a && NR<b' "$LEDGER")
|
|
269
|
+
else
|
|
270
|
+
body=$(awk -v a="$h" 'NR>a' "$LEDGER")
|
|
271
|
+
fi
|
|
272
|
+
# A herestring rather than a pipe — see check 4's note on SIGPIPE under `pipefail`.
|
|
273
|
+
if grep -qE "^- \[[ x]\] ${hid}:" <<<"$body"; then
|
|
274
|
+
printf " ✓ hand-off %s → [%s] %s resolves\n" "$src" "$target" "$hid"
|
|
275
|
+
else
|
|
276
|
+
printf " ✗ hand-off at line %s names %s in [%s], but no such entry exists there\n" \
|
|
277
|
+
"$lineno" "$hid" "$target"
|
|
278
|
+
echo " → the decision says the work moved; the target section says nothing arrived."
|
|
279
|
+
echo " Register the entry in that section (登记即路由), or drop the hand-off line."
|
|
280
|
+
bad=1
|
|
281
|
+
fi
|
|
282
|
+
done <<EOF
|
|
283
|
+
$handoffs
|
|
284
|
+
EOF
|
|
285
|
+
[ "$bad" = 0 ] || FAIL=1
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
# --- 5. exactly one trailing newline ----------------------------------------
|
|
289
|
+
echo
|
|
290
|
+
if [ -z "$(tail -c 1 "$LEDGER")" ] && [ -n "$(tail -c 2 "$LEDGER" | head -c 1)" ]; then
|
|
291
|
+
echo " ✓ single trailing newline"
|
|
292
|
+
else
|
|
293
|
+
echo " ✗ file does not end with exactly one newline"
|
|
294
|
+
echo " → append-based writers will fuse the next entry onto the previous line"
|
|
295
|
+
FAIL=1
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
# --- 6. every entry block is bounded, and carries exactly one tail --------
|
|
299
|
+
# The ledger is position-structured at TWO levels: an entry is decided by
|
|
300
|
+
# position within a section, and a section by position within the file. The
|
|
301
|
+
# checks above see neither: a misplaced tail changes no count, and check 2
|
|
302
|
+
# looks only at entry HEADERS. Observed three times — twice a `**Resolution**`
|
|
303
|
+
# landed past the end-of-file sentinel, once past a SECTION boundary. Same
|
|
304
|
+
# writer bug, different boundary; each time every check above reported OK.
|
|
305
|
+
echo
|
|
306
|
+
if ! BOUND=$(awk '
|
|
307
|
+
function flush_block() {
|
|
308
|
+
if (entry == "") return
|
|
309
|
+
if (rel != 1 || res > 1) printf "BLOCK|%s|%d|%d\n", eid, rel, res
|
|
310
|
+
}
|
|
311
|
+
/^- \[[ x]\] ISS-/ {
|
|
312
|
+
flush_block()
|
|
313
|
+
entry = $0; rel = 0; res = 0
|
|
314
|
+
eid = (match($0, /ISS-[0-9]+/)) ? substr($0, RSTART, RLENGTH) : "?"
|
|
315
|
+
next
|
|
316
|
+
}
|
|
317
|
+
/^## \[/ || /^-->/ { flush_block(); entry = ""; next }
|
|
318
|
+
/^ \*\*Related\*\*:/ { if (entry == "") printf "STRAY|%d|Related\n", NR; else rel++ }
|
|
319
|
+
/^ \*\*Resolution\*\*/ { if (entry == "") printf "STRAY|%d|Resolution\n", NR; else res++ }
|
|
320
|
+
END { flush_block() }
|
|
321
|
+
' "$LEDGER"); then
|
|
322
|
+
echo " ✗ the block-bounds check itself could not be evaluated (awk returned non-zero)"
|
|
323
|
+
echo " → a check that fails to run must NOT report as passing: an empty result"
|
|
324
|
+
echo " from a broken expression is indistinguishable from a clean ledger."
|
|
325
|
+
FAIL=1
|
|
326
|
+
elif [ -z "$BOUND" ]; then
|
|
327
|
+
echo " ✓ every entry block is bounded and carries exactly one Related line"
|
|
328
|
+
else
|
|
329
|
+
while IFS='|' read -r kind a b c; do
|
|
330
|
+
[ -z "$kind" ] && continue
|
|
331
|
+
if [ "$kind" = "BLOCK" ]; then
|
|
332
|
+
printf " ✗ %s: block carries Related=%s Resolution=%s (want 1 and <=1)\n" "$a" "$b" "$c"
|
|
333
|
+
echo " → the block does not end where it should: a tail line was split off,"
|
|
334
|
+
echo " or a foreign line was absorbed."
|
|
335
|
+
else
|
|
336
|
+
printf " ✗ line %s carries a %s OUTSIDE any entry block\n" "$a" "$b"
|
|
337
|
+
echo " → a field line was written past a boundary; it belongs to no entry,"
|
|
338
|
+
echo " so no reader will ever attribute it correctly."
|
|
339
|
+
fi
|
|
340
|
+
done <<< "$BOUND"
|
|
341
|
+
FAIL=1
|
|
342
|
+
fi
|
|
343
|
+
|
|
344
|
+
# --- 7. entry IDs are globally unique ---------------------------------------
|
|
345
|
+
# Why this exists (T206 / ISS-166): every consumer ADDRESSES an entry by its ID —
|
|
346
|
+
# `grep -qE '^- \[[x ]\] ISS-<N>:'` is the marking form this repo documents. Two entries
|
|
347
|
+
# sharing one make that address name neither of them, and NOTHING ELSE reports it: the
|
|
348
|
+
# statistics table still reconciles (bump the count and it agrees), the section checks
|
|
349
|
+
# still pass, the sentinel still holds. The collision is silent until someone reads the
|
|
350
|
+
# whole file — the failure TOOL-011 recorded for its own numbering, in a file that has
|
|
351
|
+
# since been retired while this one inherited none of it.
|
|
352
|
+
#
|
|
353
|
+
# ⚠️ `sort -n`, never `sort`: the IDs are variable width, so `ISS-100` sorts BEFORE
|
|
354
|
+
# `ISS-97` lexicographically. That is also why "take the next number" must use a numeric
|
|
355
|
+
# sort — see the same note in commands/specpro.implement.md.
|
|
356
|
+
echo
|
|
357
|
+
DUP=$(grep -oE '^- \[[x ]\] ISS-[0-9]+:' "$LEDGER" | grep -oE '[0-9]+' | sort -n | uniq -d)
|
|
358
|
+
if [ -z "$DUP" ]; then
|
|
359
|
+
echo " ✓ entry IDs are unique"
|
|
360
|
+
else
|
|
361
|
+
echo " ✗ these entry IDs are used more than once:$(printf ' %s' $DUP)"
|
|
362
|
+
echo " → an ID is how a --review-issues run ADDRESSES an entry; two entries"
|
|
363
|
+
echo " sharing one mean the address names neither of them, and the counts"
|
|
364
|
+
echo " still reconcile, so nothing else would have reported this."
|
|
365
|
+
FAIL=1
|
|
366
|
+
fi
|
|
367
|
+
|
|
368
|
+
# --- verdict -----------------------------------------------------------------
|
|
369
|
+
echo
|
|
370
|
+
if [ "$FAIL" = 0 ]; then
|
|
371
|
+
echo "✓ ledger integrity: all invariants hold"
|
|
372
|
+
exit 0
|
|
373
|
+
else
|
|
374
|
+
echo "✗ ledger integrity: VIOLATIONS FOUND (see above)"
|
|
375
|
+
exit 1
|
|
376
|
+
fi
|