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.
Files changed (28) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +28 -0
  3. package/package.json +1 -1
  4. package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
  5. package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +1 -1
  6. package/plugins/task-pipeline/skills/task-pipeline/pipeline.example.json +1 -1
  7. package/plugins/task-pipeline/skills/task-pipeline/references/acceptance.md +26 -2
  8. package/plugins/task-pipeline/skills/task-pipeline/references/artifacts.md +6 -0
  9. package/plugins/task-pipeline/skills/task-pipeline/references/audit.md +11 -0
  10. package/plugins/task-pipeline/skills/task-pipeline/references/brainstorm.md +24 -0
  11. package/plugins/task-pipeline/skills/task-pipeline/references/build.md +44 -1
  12. package/plugins/task-pipeline/skills/task-pipeline/references/companion-skills.md +10 -1
  13. package/plugins/task-pipeline/skills/task-pipeline/references/decomposition.md +9 -0
  14. package/plugins/task-pipeline/skills/task-pipeline/references/documentation.md +20 -2
  15. package/plugins/task-pipeline/skills/task-pipeline/references/gates.md +14 -0
  16. package/plugins/task-pipeline/skills/task-pipeline/references/grill.md +11 -0
  17. package/plugins/task-pipeline/skills/task-pipeline/references/hooks.md +75 -10
  18. package/plugins/task-pipeline/skills/task-pipeline/references/knowledge-graph.md +8 -0
  19. package/plugins/task-pipeline/skills/task-pipeline/references/knowledge-sources.md +13 -0
  20. package/plugins/task-pipeline/skills/task-pipeline/references/learned.md +8 -0
  21. package/plugins/task-pipeline/skills/task-pipeline/references/loop-guard.md +8 -0
  22. package/plugins/task-pipeline/skills/task-pipeline/references/planning.md +25 -0
  23. package/plugins/task-pipeline/skills/task-pipeline/references/retrospective.md +11 -0
  24. package/plugins/task-pipeline/skills/task-pipeline/references/review.md +17 -0
  25. package/plugins/task-pipeline/skills/task-pipeline/references/spec.md +22 -0
  26. package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +61 -5
  27. package/plugins/task-pipeline/skills/task-pipeline/references/tdd.md +10 -0
  28. 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 fenced code blocks: sample content is not a claim about this repository.
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
- strip_fences() {
56
+ strip_asides() {
46
57
  awk '
47
58
  /^[ \t]*(```|~~~)/ { infence = !infence; print ""; next }
48
- { if (infence) print ""; else print }
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
- strip_fences "$f" > "$TMP/s_$flat"
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
- if [ ! -e "$dir/$base" ]; then
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
- _bad=$(wc -l < "$TMP/badlinks" | tr -d ' ')
92
- err "$_bad dangling relative link(s):"; sed 's/^/ /' "$TMP/badlinks"
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 [ ! -f "$DEC_FILE" ]; then
99
- dormant "id integrity — no $DEC_FILE yet"
100
- DECS=0; OQS=0
188
+ if [ "$SHAPE" = "none" ]; then
189
+ dormant "id integrity — no decision home yet"
190
+ OQS=0
101
191
  else
102
- # Read the FENCE-STRIPPED copy: a heading inside a ``` block is sample content,
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 '\(DEC\|OQ\)-[0-9][0-9]*' | sed "s|^|$f |" >> "$TMP/refs"
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. "Next free ID" == max defined + 1 ----------
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 [ "$((_claim))" -ne "$_want" ]; then
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-$_claim)"
243
+ ok "next free $_prefix id is correct ($_prefix-$_claim_raw)"
154
244
  fi
155
245
  }
156
- check_next_free "$DEC_FILE" DEC "$TMP/dec_def"
157
- check_next_free "$OQ_FILE" OQ "$TMP/oq_def"
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
- if [ -f "$DEC_FILE" ] && grep -q 'Register size:' "$DEC_FILE" 2>/dev/null; then
162
- _stated=$(grep -o 'Register size:\** *\**[0-9][0-9]*' "$DEC_FILE" | head -1 | grep -o '[0-9][0-9]*')
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 "$DEC_FILE: states 'Register size: $_stated', computed $DECS"
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
- if [ ! -f "$DEC_FILE" ]; then
177
- dormant "propagation no $DEC_FILE yet"
178
- PROP_MISSING=0
284
+ PROP_MISSING=0
285
+ if [ "$SHAPE" = "none" ]; then
286
+ dormant "propagation — no decision home yet"
179
287
  else
180
288
  : > "$TMP/prop"
181
- _cur=""
182
- while IFS= read -r line; do
183
- case "$line" in
184
- '### DEC-'*) _cur=$(echo "$line" | grep -o 'DEC-[0-9][0-9]*') ;;
185
- *'Consequences / affects:'*)
186
- [ -n "$_cur" ] || continue
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 id doc why; do
199
- _n=$(echo "$id" | sed 's/^DEC-//' | sed 's/^0*//'); [ -n "$_n" ] || _n=0
200
- [ "$_n" -ge "$PROP_FLOOR" ] && echo "$id -> $doc ($why)" >> "$TMP/prop_new"
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 "decision(s) naming a document that does not cite them (floor DEC-$PROP_FLOOR):"
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 [ ! -f "$DEC_FILE" ]; then
215
- dormant "supersede annotations — no $DEC_FILE yet"
314
+ if [ "$SHAPE" = "none" ]; then
315
+ dormant "supersede annotations — no decision home yet"
216
316
  else
217
317
  : > "$TMP/ann"
218
- _cur=""
219
- while IFS= read -r line; do
220
- case "$line" in
221
- '### DEC-'*) _cur=$(echo "$line" | grep -o 'DEC-[0-9][0-9]*') ;;
222
- *'Supersedes:'*|*'Contradicts:'*)
223
- [ -n "$_cur" ] || continue
224
- echo "$line" | grep -o 'DEC-[0-9][0-9]*' | while IFS= read -r target; do
225
- [ "$target" = "$_cur" ] && continue
226
- _status=$(awk -v t="### $target " '
227
- index($0, t) == 1 { found = 1; next }
228
- found && /^### DEC-/ { exit }
229
- found && /Status:/ { print; exit }
230
- ' "$(flat_of "$DEC_FILE")")
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
- if [ ! -f "$DEC_FILE" ]; then
251
- dormant "retired residue no $DEC_FILE yet"
252
- RESIDUE=0
343
+ RESIDUE=0
344
+ if [ "$SHAPE" = "none" ]; then
345
+ dormant "retired residue — no decision home yet"
253
346
  else
254
347
  : > "$TMP/retired"
255
- _cur=""
256
- while IFS= read -r line; do
257
- case "$line" in
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 < "$(flat_of "$DEC_FILE")"
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 [ -f "$DEC_FILE" ]; then
383
+ if [ "$SHAPE" = "none" ]; then
384
+ dormant "decision status vocabulary — no decision home yet"
385
+ else
298
386
  : > "$TMP/vocab"
299
- grep -n 'Status:' "$(flat_of "$DEC_FILE")" 2>/dev/null | while IFS= read -r hit; do
300
- case "$hit" in
301
- *Accepted*|*Superseded\ by*|*Reversed*|*'`Status`'*) ;;
302
- *) echo "$DEC_FILE:${hit%%:*}: unknown decision status -> ${hit#*Status:}" >> "$TMP/vocab" ;;
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→DEC-*|*Dropped*) ;;
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 '`[^`]*\.md`' | tr -d '`' | sort -u > "$TMP/map"
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
- [ -e "$doc" ] || echo "$MAP_FILE names $doc, which does not exist" >> "$TMP/mapmiss"
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