task-pipeline-skill 1.7.1 → 1.8.0
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.
- package/CHANGELOG.md +130 -0
- package/README.md +28 -0
- package/package.json +1 -1
- package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/pipeline.example.json +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/acceptance.md +26 -2
- package/plugins/task-pipeline/skills/task-pipeline/references/artifacts.md +6 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/audit.md +11 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/brainstorm.md +24 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/build.md +44 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/companion-skills.md +10 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/decomposition.md +9 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/documentation.md +20 -2
- package/plugins/task-pipeline/skills/task-pipeline/references/gates.md +14 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/grill.md +11 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/hooks.md +75 -10
- package/plugins/task-pipeline/skills/task-pipeline/references/knowledge-graph.md +8 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/knowledge-sources.md +13 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/learned.md +8 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/loop-guard.md +8 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/planning.md +25 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/retrospective.md +11 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/review.md +17 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/spec.md +22 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +61 -5
- package/plugins/task-pipeline/skills/task-pipeline/references/tdd.md +10 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/docgate.sh +194 -99
|
@@ -18,12 +18,20 @@
|
|
|
18
18
|
# PROGRESSIVE ARMING: a section whose input does not exist yet prints
|
|
19
19
|
# "dormant: … — no <artefact> yet" and does NOT fail. Dormant is visible so it is
|
|
20
20
|
# not forgotten, and green so a freshly seeded project does not start red.
|
|
21
|
+
#
|
|
22
|
+
# TWO REGISTER SHAPES, ONE CONTRACT: the project's decision home is either
|
|
23
|
+
# docs/DECISIONS.md (ids DEC-####) or docs/adr/NNNN-slug.md (ids ADR-NNNN). Every
|
|
24
|
+
# section below reads a NORMALISED INDEX built from whichever exists, so neither
|
|
25
|
+
# shape is a second-class citizen. Having both is itself an error: one home per
|
|
26
|
+
# project. Reading only one shape is how a fully populated ADR register sat behind
|
|
27
|
+
# eight green "dormant" lines while a planted violation went uncaught.
|
|
21
28
|
|
|
22
29
|
set -u
|
|
23
30
|
|
|
24
31
|
FAIL=0
|
|
25
32
|
DOCS_DIR=${DOCS_DIR:-docs}
|
|
26
33
|
DEC_FILE=${DEC_FILE:-$DOCS_DIR/DECISIONS.md}
|
|
34
|
+
ADR_DIR=${ADR_DIR:-$DOCS_DIR/adr}
|
|
27
35
|
OQ_FILE=${OQ_FILE:-$DOCS_DIR/OPEN_QUESTIONS.md}
|
|
28
36
|
MAP_FILE=${MAP_FILE:-$DOCS_DIR/DOCMAP.md}
|
|
29
37
|
RETRO_GLOB=${RETRO_GLOB:-$DOCS_DIR/superpowers}
|
|
@@ -40,12 +48,31 @@ ok() { echo "ok: $*"; }
|
|
|
40
48
|
skipmsg() { echo "skip: $*"; }
|
|
41
49
|
dormant() { echo "dormant: $*"; }
|
|
42
50
|
|
|
43
|
-
# Strip
|
|
51
|
+
# Strip what a reader never sees: fenced code blocks AND html comments. Sample
|
|
52
|
+
# content is not a claim about this repository. The comment half is not fussiness —
|
|
53
|
+
# a status line carrying `<!-- or: Superseded by ADR-0012 -->` made an entry read as
|
|
54
|
+
# retired and invented an undefined id, from one aside nobody renders.
|
|
44
55
|
# awk, because sed -i is not portable and this must run identically everywhere.
|
|
45
|
-
|
|
56
|
+
strip_asides() {
|
|
46
57
|
awk '
|
|
47
58
|
/^[ \t]*(```|~~~)/ { infence = !infence; print ""; next }
|
|
48
|
-
{
|
|
59
|
+
infence { print ""; next }
|
|
60
|
+
{
|
|
61
|
+
line = $0
|
|
62
|
+
# A comment carried over from an earlier line.
|
|
63
|
+
if (incomment) {
|
|
64
|
+
if (match(line, /-->/)) { line = substr(line, RSTART + RLENGTH); incomment = 0 }
|
|
65
|
+
else { print ""; next }
|
|
66
|
+
}
|
|
67
|
+
# Comments that open and close on this line.
|
|
68
|
+
while (match(line, /<!--.*-->/)) sub(/<!--.*-->/, "", line)
|
|
69
|
+
# An opener with no closer: KEEP THE PREFIX. Dropping the whole line threw
|
|
70
|
+
# away the "- **Status:** Accepted" that preceded the comment, and the entry
|
|
71
|
+
# then had no status at all — the id vanished from the index and every
|
|
72
|
+
# document citing it was reported as citing something undefined.
|
|
73
|
+
if (match(line, /<!--/)) { line = substr(line, 1, RSTART - 1); incomment = 1 }
|
|
74
|
+
print line
|
|
75
|
+
}
|
|
49
76
|
' "$1"
|
|
50
77
|
}
|
|
51
78
|
|
|
@@ -63,17 +90,82 @@ fi
|
|
|
63
90
|
while IFS= read -r f; do
|
|
64
91
|
[ -f "$f" ] || continue
|
|
65
92
|
flat=$(echo "$f" | tr '/' '_')
|
|
66
|
-
|
|
93
|
+
strip_asides "$f" > "$TMP/s_$flat"
|
|
67
94
|
done < "$TMP/files"
|
|
68
95
|
|
|
69
96
|
flat_of() { echo "$TMP/s_$(echo "$1" | tr '/' '_')"; }
|
|
70
97
|
|
|
98
|
+
# ---------- 0. the decision home — exactly one, and which shape ----------
|
|
99
|
+
# entries: ID <TAB> FILE <TAB> STATUS-LINE edges: SRC <TAB> MARKER <TAB> TARGET
|
|
100
|
+
# conseq: ID <TAB> DOC
|
|
101
|
+
: > "$TMP/entries"; : > "$TMP/edges"; : > "$TMP/conseq"
|
|
102
|
+
SHAPE="none"; ID_PREFIX=""
|
|
103
|
+
|
|
104
|
+
have_reg=0; [ -f "$DEC_FILE" ] && have_reg=1
|
|
105
|
+
have_adr=0
|
|
106
|
+
if [ -d "$ADR_DIR" ]; then
|
|
107
|
+
find "$ADR_DIR" -type f -name '[0-9][0-9][0-9][0-9]-*.md' 2>/dev/null | sort > "$TMP/adrfiles"
|
|
108
|
+
[ -s "$TMP/adrfiles" ] && have_adr=1
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
if [ "$have_reg" = "1" ] && [ "$have_adr" = "1" ]; then
|
|
112
|
+
err "two decision homes: $DEC_FILE and $ADR_DIR both hold entries — one project, one register (references/documentation.md)"
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# Pull the six owed fields out of one entry body on stdin, for id $1 in file $2.
|
|
116
|
+
harvest_entry() { # id file (body on stdin)
|
|
117
|
+
_id=$1; _file=$2
|
|
118
|
+
while IFS= read -r line; do
|
|
119
|
+
case "$line" in
|
|
120
|
+
*'Status:'*)
|
|
121
|
+
grep -q "^$_id " "$TMP/entries" 2>/dev/null || \
|
|
122
|
+
printf '%s\t%s\t%s\n' "$_id" "$_file" "$line" >> "$TMP/entries" ;;
|
|
123
|
+
*'Consequences / affects:'*)
|
|
124
|
+
echo "$line" | grep -o '`[^`]*`' | tr -d '`' | while IFS= read -r doc; do
|
|
125
|
+
case "$doc" in *.md) printf '%s\t%s\n' "$_id" "$doc" >> "$TMP/conseq" ;; esac
|
|
126
|
+
done ;;
|
|
127
|
+
*'Supersedes:'*|*'Contradicts:'*|*'Refines:'*)
|
|
128
|
+
_mk=$(echo "$line" | grep -o 'Supersedes\|Contradicts\|Refines' | head -1)
|
|
129
|
+
echo "$line" | grep -o '\(DEC\|ADR\)-[0-9][0-9]*' | while IFS= read -r tgt; do
|
|
130
|
+
[ "$tgt" = "$_id" ] || printf '%s\t%s\t%s\n' "$_id" "$_mk" "$tgt" >> "$TMP/edges"
|
|
131
|
+
done ;;
|
|
132
|
+
esac
|
|
133
|
+
done
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if [ "$have_reg" = "1" ]; then
|
|
137
|
+
SHAPE="register"; ID_PREFIX="DEC"
|
|
138
|
+
# Split the fence-stripped register into one body per "### DEC-####" heading.
|
|
139
|
+
awk -v out="$TMP" '
|
|
140
|
+
/^### DEC-[0-9]+/ { id=$2; sub(/[^A-Za-z0-9-].*/,"",id); n++; f=out "/e_" n; ids[n]=id }
|
|
141
|
+
n { print > f }
|
|
142
|
+
END { for (i=1;i<=n;i++) print ids[i] > (out "/e_ids") }
|
|
143
|
+
' "$(flat_of "$DEC_FILE")"
|
|
144
|
+
if [ -f "$TMP/e_ids" ]; then
|
|
145
|
+
_n=0
|
|
146
|
+
while IFS= read -r _id; do
|
|
147
|
+
_n=$((_n + 1))
|
|
148
|
+
harvest_entry "$_id" "$DEC_FILE" < "$TMP/e_$_n"
|
|
149
|
+
done < "$TMP/e_ids"
|
|
150
|
+
fi
|
|
151
|
+
elif [ "$have_adr" = "1" ]; then
|
|
152
|
+
SHAPE="adr"; ID_PREFIX="ADR"
|
|
153
|
+
while IFS= read -r af; do
|
|
154
|
+
_num=$(basename "$af" | sed 's/^\([0-9][0-9]*\)-.*/\1/')
|
|
155
|
+
harvest_entry "ADR-$_num" "$af" < "$(flat_of "$af")"
|
|
156
|
+
done < "$TMP/adrfiles"
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
DECS=$(wc -l < "$TMP/entries" | tr -d ' ')
|
|
160
|
+
case "$SHAPE" in
|
|
161
|
+
none) dormant "decision home — neither $DEC_FILE nor $ADR_DIR/NNNN-*.md yet" ;;
|
|
162
|
+
*) ok "decision home: $SHAPE ($DECS entr$([ "$DECS" = "1" ] && echo y || echo ies), ids $ID_PREFIX-####)" ;;
|
|
163
|
+
esac
|
|
164
|
+
|
|
71
165
|
# ---------- 1. relative links resolve ----------
|
|
72
|
-
_bad=0
|
|
73
166
|
while IFS= read -r f; do
|
|
74
167
|
[ -f "$f" ] || continue
|
|
75
168
|
dir=$(dirname "$f")
|
|
76
|
-
# grep -o on the link target; keep the line number for the message.
|
|
77
169
|
grep -n -o '](\([^) ]*\))' "$(flat_of "$f")" 2>/dev/null |
|
|
78
170
|
sed 's/](\(.*\))/\1/' |
|
|
79
171
|
while IFS=: read -r ln target; do
|
|
@@ -82,28 +174,22 @@ while IFS= read -r f; do
|
|
|
82
174
|
esac
|
|
83
175
|
base=${target%%#*}
|
|
84
176
|
[ -n "$base" ] || continue
|
|
85
|
-
|
|
86
|
-
echo "$f:$ln: dangling link -> $target" >> "$TMP/badlinks"
|
|
87
|
-
fi
|
|
177
|
+
[ -e "$dir/$base" ] || echo "$f:$ln: dangling link -> $target" >> "$TMP/badlinks"
|
|
88
178
|
done
|
|
89
179
|
done < "$TMP/files"
|
|
90
180
|
if [ -s "$TMP/badlinks" ] 2>/dev/null; then
|
|
91
|
-
|
|
92
|
-
|
|
181
|
+
err "$(wc -l < "$TMP/badlinks" | tr -d ' ') dangling relative link(s):"
|
|
182
|
+
sed 's/^/ /' "$TMP/badlinks"
|
|
93
183
|
else
|
|
94
184
|
ok "relative links resolve ($FILE_COUNT files)"
|
|
95
185
|
fi
|
|
96
186
|
|
|
97
187
|
# ---------- 2. every id referenced is defined ----------
|
|
98
|
-
if [
|
|
99
|
-
dormant "id integrity — no
|
|
100
|
-
|
|
188
|
+
if [ "$SHAPE" = "none" ]; then
|
|
189
|
+
dormant "id integrity — no decision home yet"
|
|
190
|
+
OQS=0
|
|
101
191
|
else
|
|
102
|
-
|
|
103
|
-
# not a definition. Reading the raw file here counts the format example as an
|
|
104
|
-
# entry, and then "Next free ID" is wrong against a decision that never existed.
|
|
105
|
-
grep -o '^### DEC-[0-9][0-9]*' "$(flat_of "$DEC_FILE")" | sed 's/^### //' | sort -u > "$TMP/dec_def"
|
|
106
|
-
DECS=$(wc -l < "$TMP/dec_def" | tr -d ' ')
|
|
192
|
+
cut -f1 "$TMP/entries" | sort -u > "$TMP/dec_def"
|
|
107
193
|
: > "$TMP/oq_def"
|
|
108
194
|
[ -f "$OQ_FILE" ] && grep -o '^| *OQ-[0-9][0-9]*' "$(flat_of "$OQ_FILE")" | sed 's/^| *//' | sort -u > "$TMP/oq_def"
|
|
109
195
|
OQS=$(wc -l < "$TMP/oq_def" | tr -d ' ')
|
|
@@ -113,7 +199,7 @@ else
|
|
|
113
199
|
while IFS= read -r f; do
|
|
114
200
|
[ -f "$f" ] || continue
|
|
115
201
|
grep -v 'Next free ID' "$(flat_of "$f")" 2>/dev/null |
|
|
116
|
-
grep -o
|
|
202
|
+
grep -o "\($ID_PREFIX\|OQ\)-[0-9][0-9]*" | sed "s|^|$f |" >> "$TMP/refs"
|
|
117
203
|
done < "$TMP/files"
|
|
118
204
|
|
|
119
205
|
: > "$TMP/undef"
|
|
@@ -127,7 +213,10 @@ else
|
|
|
127
213
|
fi
|
|
128
214
|
fi
|
|
129
215
|
|
|
130
|
-
# ---------- 3.
|
|
216
|
+
# ---------- 3. the id allocator is sound ----------
|
|
217
|
+
# Register shape: a stated "Next free ID" must equal max defined + 1.
|
|
218
|
+
# ADR shape: there is no such line — the filename IS the allocator, so the sound
|
|
219
|
+
# check is that no two files claim one number.
|
|
131
220
|
check_next_free() {
|
|
132
221
|
_file=$1; _prefix=$2; _deffile=$3
|
|
133
222
|
[ -f "$_file" ] || { dormant "next-free-$_prefix — no $_file yet"; return; }
|
|
@@ -141,27 +230,46 @@ check_next_free() {
|
|
|
141
230
|
# an octal digit, the expansion errors, the `if` takes its else branch and the
|
|
142
231
|
# check prints ok. It passed for every id ending 0-7 and was silent for 8 and 9.
|
|
143
232
|
# Found by the probe; the check was wrong, not the probe.
|
|
233
|
+
_claim_raw=$_claim
|
|
144
234
|
_claim=$(echo "$_claim" | sed 's/^0*//'); [ -n "$_claim" ] || _claim=0
|
|
145
235
|
if [ ! -s "$_deffile" ]; then _max=0; else
|
|
146
236
|
_max=$(sed "s/^$_prefix-//" "$_deffile" | sed 's/^0*//' | sort -n | tail -1)
|
|
147
237
|
[ -n "${_max:-}" ] || _max=0
|
|
148
238
|
fi
|
|
149
239
|
_want=$((_max + 1))
|
|
150
|
-
if [ "$
|
|
240
|
+
if [ "$_claim" -ne "$_want" ]; then
|
|
151
241
|
err "$_file: 'Next free ID' claims $_prefix-$_claim, highest defined is $_max (expected $_want)"
|
|
152
242
|
else
|
|
153
|
-
ok "next free $_prefix id is correct ($_prefix-$
|
|
243
|
+
ok "next free $_prefix id is correct ($_prefix-$_claim_raw)"
|
|
154
244
|
fi
|
|
155
245
|
}
|
|
156
|
-
|
|
157
|
-
check_next_free "$
|
|
246
|
+
case "$SHAPE" in
|
|
247
|
+
register) check_next_free "$DEC_FILE" DEC "$TMP/dec_def" ;;
|
|
248
|
+
adr)
|
|
249
|
+
# Count from the FILENAMES, not from the entry index. The index keeps one row
|
|
250
|
+
# per id on purpose (an entry has one status line), and that dedupe silently
|
|
251
|
+
# swallowed the very thing this check looks for: a second file claiming a
|
|
252
|
+
# number already taken. The filename is the allocator, so the filename is what
|
|
253
|
+
# gets counted. The check was wrong, not the probe.
|
|
254
|
+
_dupes=$(sed 's|.*/||; s|^\([0-9][0-9]*\)-.*|\1|' "$TMP/adrfiles" | sort | uniq -d | tr '\n' ' ')
|
|
255
|
+
_adr_n=$(wc -l < "$TMP/adrfiles" | tr -d ' ')
|
|
256
|
+
if [ -n "$(echo "$_dupes" | tr -d ' ')" ]; then
|
|
257
|
+
err "duplicate ADR number(s) — two files claim one id: $_dupes"
|
|
258
|
+
else
|
|
259
|
+
ok "ADR numbers are unique ($_adr_n files)"
|
|
260
|
+
fi ;;
|
|
261
|
+
*) dormant "id allocator — no decision home yet" ;;
|
|
262
|
+
esac
|
|
263
|
+
check_next_free "$OQ_FILE" OQ "$TMP/oq_def"
|
|
158
264
|
|
|
159
265
|
# ---------- 4. a stated register size equals the computed one ----------
|
|
160
266
|
# Compute, never restate: a number written in prose is a number that goes stale.
|
|
161
|
-
|
|
162
|
-
|
|
267
|
+
_size_src=""
|
|
268
|
+
[ "$SHAPE" = "register" ] && _size_src=$DEC_FILE
|
|
269
|
+
if [ -n "$_size_src" ] && grep -q 'Register size:' "$_size_src" 2>/dev/null; then
|
|
270
|
+
_stated=$(grep -o 'Register size:\** *\**[0-9][0-9]*' "$_size_src" | head -1 | grep -o '[0-9][0-9]*')
|
|
163
271
|
if [ "${_stated:-x}" != "$DECS" ]; then
|
|
164
|
-
err "$
|
|
272
|
+
err "$_size_src: states 'Register size: $_stated', computed $DECS"
|
|
165
273
|
else
|
|
166
274
|
ok "stated register size matches the computed one ($DECS)"
|
|
167
275
|
fi
|
|
@@ -173,35 +281,27 @@ fi
|
|
|
173
281
|
# A document named in an entry's "Consequences / affects:" line must cite that
|
|
174
282
|
# entry. Writing down where a decision must propagate and then not propagating is
|
|
175
283
|
# the exact failure the loop exists to prevent.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
284
|
+
PROP_MISSING=0
|
|
285
|
+
if [ "$SHAPE" = "none" ]; then
|
|
286
|
+
dormant "propagation — no decision home yet"
|
|
179
287
|
else
|
|
180
288
|
: > "$TMP/prop"
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
echo "$line" | grep -o '`[^`]*`' | tr -d '`' | while IFS= read -r doc; do
|
|
188
|
-
case "$doc" in *.md) ;; *) continue ;; esac
|
|
189
|
-
[ -f "$doc" ] || { echo "$_cur $doc MISSINGFILE" >> "$TMP/prop"; continue; }
|
|
190
|
-
if grep -q "$_cur" "$doc"; then :; else echo "$_cur $doc NOCITE" >> "$TMP/prop"; fi
|
|
191
|
-
done ;;
|
|
192
|
-
esac
|
|
193
|
-
done < "$(flat_of "$DEC_FILE")"
|
|
194
|
-
PROP_MISSING=0
|
|
289
|
+
while IFS="$(printf '\t')" read -r id doc; do
|
|
290
|
+
[ -n "${doc:-}" ] || continue
|
|
291
|
+
if [ ! -f "$doc" ]; then echo "$id -> $doc (MISSINGFILE)" >> "$TMP/prop"
|
|
292
|
+
elif grep -q "$id" "$doc"; then :
|
|
293
|
+
else echo "$id -> $doc (NOCITE)" >> "$TMP/prop"; fi
|
|
294
|
+
done < "$TMP/conseq"
|
|
195
295
|
[ -f "$TMP/prop" ] && PROP_MISSING=$(wc -l < "$TMP/prop" | tr -d ' ')
|
|
196
296
|
: > "$TMP/prop_new"
|
|
197
297
|
if [ "$PROP_MISSING" -gt 0 ]; then
|
|
198
|
-
while read -r
|
|
199
|
-
_n=$(echo "$
|
|
200
|
-
[ "$_n" -ge "$PROP_FLOOR" ] && echo "$
|
|
298
|
+
while IFS= read -r row; do
|
|
299
|
+
_n=$(echo "$row" | grep -o '[0-9][0-9]*' | head -1 | sed 's/^0*//'); [ -n "$_n" ] || _n=0
|
|
300
|
+
[ "$_n" -ge "$PROP_FLOOR" ] && echo "$row" >> "$TMP/prop_new"
|
|
201
301
|
done < "$TMP/prop"
|
|
202
302
|
fi
|
|
203
303
|
if [ -s "$TMP/prop_new" ] 2>/dev/null; then
|
|
204
|
-
err "
|
|
304
|
+
err "entr(y|ies) naming a document that does not cite them (floor $ID_PREFIX-$PROP_FLOOR):"
|
|
205
305
|
sed 's/^/ /' "$TMP/prop_new"
|
|
206
306
|
else
|
|
207
307
|
ok "consequences propagate (backlog below the floor: $PROP_MISSING)"
|
|
@@ -211,30 +311,23 @@ fi
|
|
|
211
311
|
# ---------- 6. supersede / contradict annotates the target ----------
|
|
212
312
|
# One word for "adds to" and "replaces a clause of" is unenforceable, so the
|
|
213
313
|
# markers are distinct and only two of them oblige the target to say so.
|
|
214
|
-
if [
|
|
215
|
-
dormant "supersede annotations — no
|
|
314
|
+
if [ "$SHAPE" = "none" ]; then
|
|
315
|
+
dormant "supersede annotations — no decision home yet"
|
|
216
316
|
else
|
|
217
317
|
: > "$TMP/ann"
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
case "$_status" in
|
|
232
|
-
*"$_cur"*) ;;
|
|
233
|
-
*) echo "$target: status line does not record that $_cur retires or contradicts it" >> "$TMP/ann" ;;
|
|
234
|
-
esac
|
|
235
|
-
done ;;
|
|
236
|
-
esac
|
|
237
|
-
done < "$(flat_of "$DEC_FILE")"
|
|
318
|
+
while IFS="$(printf '\t')" read -r src marker target; do
|
|
319
|
+
[ -n "${target:-}" ] || continue
|
|
320
|
+
[ "$marker" = "Refines" ] && continue # additive: no annotation owed
|
|
321
|
+
_status=$(grep "^$target " "$TMP/entries" | head -1 | cut -f3)
|
|
322
|
+
if [ -z "$_status" ]; then
|
|
323
|
+
echo "$src $marker $target: target is not a defined entry" >> "$TMP/ann"
|
|
324
|
+
else
|
|
325
|
+
case "$_status" in
|
|
326
|
+
*"$src"*) ;;
|
|
327
|
+
*) echo "$target: status line does not record that $src ${marker}s it" >> "$TMP/ann" ;;
|
|
328
|
+
esac
|
|
329
|
+
fi
|
|
330
|
+
done < "$TMP/edges"
|
|
238
331
|
if [ -s "$TMP/ann" ] 2>/dev/null; then
|
|
239
332
|
err "unannotated supersede/contradict target(s):"; sed 's/^/ /' "$TMP/ann"
|
|
240
333
|
else
|
|
@@ -247,29 +340,22 @@ fi
|
|
|
247
340
|
# is a line: one marker on it exempts every id on it. That is a real blind spot,
|
|
248
341
|
# measured and accepted — a tighter window produced mostly noise, and a gate that
|
|
249
342
|
# is mostly noise is a gate people switch off.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
343
|
+
RESIDUE=0
|
|
344
|
+
if [ "$SHAPE" = "none" ]; then
|
|
345
|
+
dormant "retired residue — no decision home yet"
|
|
253
346
|
else
|
|
254
347
|
: > "$TMP/retired"
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
'### DEC-'*) _cur=$(echo "$line" | grep -o 'DEC-[0-9][0-9]*') ;;
|
|
259
|
-
*'Status:'*)
|
|
260
|
-
[ -n "$_cur" ] || continue
|
|
261
|
-
case "$line" in
|
|
262
|
-
*Superseded\ by*|*Reversed*) echo "$_cur" >> "$TMP/retired" ;;
|
|
263
|
-
esac
|
|
264
|
-
_cur="" ;;
|
|
348
|
+
while IFS="$(printf '\t')" read -r id file status; do
|
|
349
|
+
case "$status" in
|
|
350
|
+
*Superseded\ by*|*Reversed*) echo "$id" >> "$TMP/retired" ;;
|
|
265
351
|
esac
|
|
266
|
-
done < "$
|
|
267
|
-
RESIDUE=0
|
|
352
|
+
done < "$TMP/entries"
|
|
268
353
|
if [ -s "$TMP/retired" ] 2>/dev/null; then
|
|
269
354
|
: > "$TMP/res"
|
|
270
355
|
while IFS= read -r f; do
|
|
271
356
|
[ -f "$f" ] || continue
|
|
272
357
|
[ "$f" = "$DEC_FILE" ] && continue
|
|
358
|
+
case "$f" in "$ADR_DIR"/*) continue ;; esac
|
|
273
359
|
while IFS= read -r rid; do
|
|
274
360
|
grep -n "$rid" "$(flat_of "$f")" 2>/dev/null | while IFS=: read -r ln text; do
|
|
275
361
|
case "$text" in
|
|
@@ -294,28 +380,28 @@ fi
|
|
|
294
380
|
# ---------- 8. status vocabularies are closed ----------
|
|
295
381
|
# An unrecognised status is worse than a missing one: it looks answered, and every
|
|
296
382
|
# check on that row skips in silence.
|
|
297
|
-
if [
|
|
383
|
+
if [ "$SHAPE" = "none" ]; then
|
|
384
|
+
dormant "decision status vocabulary — no decision home yet"
|
|
385
|
+
else
|
|
298
386
|
: > "$TMP/vocab"
|
|
299
|
-
|
|
300
|
-
case "$
|
|
301
|
-
*Accepted*|*Superseded\ by*|*Reversed
|
|
302
|
-
*) echo "$
|
|
387
|
+
while IFS="$(printf '\t')" read -r id file status; do
|
|
388
|
+
case "$status" in
|
|
389
|
+
*Accepted*|*Superseded\ by*|*Reversed*) ;;
|
|
390
|
+
*) echo "$file: $id has an unknown status ->${status#*Status:}" >> "$TMP/vocab" ;;
|
|
303
391
|
esac
|
|
304
|
-
done
|
|
392
|
+
done < "$TMP/entries"
|
|
305
393
|
if [ -s "$TMP/vocab" ] 2>/dev/null; then
|
|
306
394
|
err "decision status vocabulary:"; sed 's/^/ /' "$TMP/vocab"
|
|
307
395
|
else
|
|
308
396
|
ok "decision statuses are inside the closed vocabulary"
|
|
309
397
|
fi
|
|
310
|
-
else
|
|
311
|
-
dormant "decision status vocabulary — no $DEC_FILE yet"
|
|
312
398
|
fi
|
|
313
399
|
|
|
314
400
|
if [ -f "$OQ_FILE" ]; then
|
|
315
401
|
: > "$TMP/oqvocab"
|
|
316
402
|
grep -n '^| *OQ-[0-9]' "$(flat_of "$OQ_FILE")" 2>/dev/null | while IFS= read -r row; do
|
|
317
403
|
case "$row" in
|
|
318
|
-
*'| Open '*|*'| Open|'*|*Open\ \|*|*Resolved
|
|
404
|
+
*'| Open '*|*'| Open|'*|*Open\ \|*|*Resolved→*|*Dropped*) ;;
|
|
319
405
|
*) echo "$OQ_FILE:${row%%:*}: unknown question status" >> "$TMP/oqvocab" ;;
|
|
320
406
|
esac
|
|
321
407
|
done
|
|
@@ -362,19 +448,28 @@ else
|
|
|
362
448
|
# Forward direction is scoped to the "## Registers" table: that table is a CLAIM
|
|
363
449
|
# about what exists. The SSOT table below it legitimately names documents a young
|
|
364
450
|
# project has not written yet, and failing on those would make the gate seed red.
|
|
451
|
+
# TABLE ROWS ONLY. Reading every backtick in the section swept up the prose note
|
|
452
|
+
# under the table ("an existing docs/adr/ IS the register") and reported it as a
|
|
453
|
+
# missing file — a claim the note never made. A row is a claim; a sentence is not.
|
|
365
454
|
: > "$TMP/map"
|
|
366
|
-
awk '/^## Registers/ { on = 1; next } on && /^## / { exit } on { print }' \
|
|
367
|
-
"$(flat_of "$MAP_FILE")" | grep -o '`[^`]
|
|
455
|
+
awk '/^## Registers/ { on = 1; next } on && /^## / { exit } on && /^\|/ { print }' \
|
|
456
|
+
"$(flat_of "$MAP_FILE")" | grep -o '`[^`]*`' | tr -d '`' | sort -u > "$TMP/map"
|
|
368
457
|
: > "$TMP/mapmiss"
|
|
369
458
|
while IFS= read -r doc; do
|
|
370
459
|
case "$doc" in *'<'*|*'>'*) continue ;; esac
|
|
371
|
-
|
|
460
|
+
case "$doc" in *.md|*/) ;; *) continue ;; esac
|
|
461
|
+
[ -e "$doc" ] || [ -e "${doc%/}" ] ||
|
|
462
|
+
echo "$MAP_FILE names $doc, which does not exist" >> "$TMP/mapmiss"
|
|
372
463
|
done < "$TMP/map"
|
|
373
464
|
for reg in "$DEC_FILE" "$OQ_FILE"; do
|
|
374
465
|
[ -f "$reg" ] || continue
|
|
375
466
|
grep -q "$(basename "$reg")" "$TMP/map" ||
|
|
376
467
|
echo "$reg exists but $MAP_FILE never names it" >> "$TMP/mapmiss"
|
|
377
468
|
done
|
|
469
|
+
if [ "$SHAPE" = "adr" ]; then
|
|
470
|
+
grep -q "adr" "$TMP/map" ||
|
|
471
|
+
echo "$ADR_DIR is this project's decision home but $MAP_FILE never names it" >> "$TMP/mapmiss"
|
|
472
|
+
fi
|
|
378
473
|
if [ -s "$TMP/mapmiss" ] 2>/dev/null; then
|
|
379
474
|
err "doc map / register disagreement:"; sed 's/^/ /' "$TMP/mapmiss"
|
|
380
475
|
else
|
|
@@ -387,5 +482,5 @@ if [ "$FAIL" -ne 0 ]; then
|
|
|
387
482
|
echo "FAIL: documentation gate"
|
|
388
483
|
exit 1
|
|
389
484
|
fi
|
|
390
|
-
echo "OK: documentation gate — ${DECS:-0} decisions · ${OQS:-0} open questions · propagation backlog ${PROP_MISSING:-0} (floor $PROP_FLOOR) · retired residue ${RESIDUE:-0} (floor $RESIDUE_FLOOR)"
|
|
485
|
+
echo "OK: documentation gate — shape $SHAPE · ${DECS:-0} decisions · ${OQS:-0} open questions · propagation backlog ${PROP_MISSING:-0} (floor $PROP_FLOOR) · retired residue ${RESIDUE:-0} (floor $RESIDUE_FLOOR)"
|
|
391
486
|
exit 0
|