#!/usr/bin/env bash # check-cards-against-spec — verify scenario cards carry their spec table's # falsification lines verbatim. See authoring-cards-from-a-spec.md. set -euo pipefail usage() { cat <<'EOF' Usage: check-cards-against-spec Verifies the spec's "E2E scenario cards" table against the cards directory: 1. table parses (>=1 row; non-empty Card and Falsification cells) 2. every row has /.md 3. every card contains its Falsification line verbatim (whitespace-normalized, fixed-string, case-sensitive) 4. every card has **What this covers** (bold inline) and ## headings Pre-state, Steps, Expected, Cleanup (Sharp edges not required) 5. extra cards in are reported as warnings, not failures Exit: 0 all pass; 1 check failed; 2 no "E2E scenario cards" table; 64 usage. EOF } [ "${1:-}" = "--help" ] && { usage; exit 0; } [ $# -eq 2 ] || { usage >&2; exit 64; } SPEC="$1"; CARDS="$2" [ -f "$SPEC" ] || { echo "error: spec not found: $SPEC" >&2; exit 64; } [ -d "$CARDS" ] || { echo "error: cards dir not found: $CARDS" >&2; exit 64; } FAILURES=0 fail() { echo "FAIL: $1"; FAILURES=$((FAILURES + 1)); } warn() { echo "warn: $1"; } # Collapse every whitespace run to one space; trim ends. (Normative per the # design spec: markdown re-wrapping must not defeat the verbatim check.) normalize() { tr -s '[:space:]' ' ' | sed -e 's/^ //' -e 's/ $//'; } # Text of the card's Expected section only (case-insensitive heading match, # any ##+ level; section ends at the next heading or EOF). expected_section() { awk ' /^#{1,6}[[:space:]]/ { low = tolower($0) if (low ~ /^#+[[:space:]]*expected[[:space:]]*$/) { insec = 1; next } if (insec) exit } insec { print } ' "$1" } # --- extract the first table under the (case-insensitive) heading ---------- TABLE="$(awk ' /^#{1,6}[[:space:]]/ { h = $0; sub(/^#+[[:space:]]*/, "", h); sub(/[[:space:]]+$/, "", h) if (tolower(h) == "e2e scenario cards") { insec = 1; next } if (insec) exit } insec && /^[[:space:]]*\|/ { intable = 1; print; next } insec && intable { exit } ' "$SPEC")" if [ -z "$TABLE" ]; then echo "no scenario table: $SPEC has no \"E2E scenario cards\" heading with a table under it" >&2 echo "(heading must be exactly \"E2E scenario cards\" — no numbering or extra words)" >&2 exit 2 fi # --- parse: protect escaped pipes, split rows into cells ------------------- US=$'\x1f' CARD_COL=-1; FALS_COL=-1; ROWS=0 declare -a ROW_CARD ROW_FALS lineno=0 while IFS= read -r line; do lineno=$((lineno + 1)) esc="${line//\\|/$US}" IFS='|' read -r -a cells <<< "$esc" # drop leading/trailing empty fields produced by the outer pipes trimmed=() for c in "${cells[@]}"; do c="${c//$US/|}" c="$(printf '%s' "$c" | normalize)" trimmed+=("$c") done # cells[0] is empty (before first |); last may be empty too if [ "$lineno" -eq 1 ]; then for i in "${!trimmed[@]}"; do low="$(printf '%s' "${trimmed[$i]}" | tr '[:upper:]' '[:lower:]')" [ "$low" = "card" ] && CARD_COL=$i [ "$low" = "falsification" ] && FALS_COL=$i done continue fi # separator row: cells of dashes/colons only joined="$(printf '%s' "${trimmed[*]}" | tr -d ' :-')" [ -z "$joined" ] && continue if [ "$CARD_COL" -lt 0 ] || [ "$FALS_COL" -lt 0 ]; then fail "table header must name Card and Falsification columns" break fi card="${trimmed[$CARD_COL]:-}" falsif="${trimmed[$FALS_COL]:-}" card="${card//\`/}" # tolerate `card-name` backticks in the cell if [ -z "$card" ] || [ -z "$falsif" ]; then fail "row $lineno: empty Card or Falsification cell" continue fi ROW_CARD[$ROWS]="$card"; ROW_FALS[$ROWS]="$falsif"; ROWS=$((ROWS + 1)) done <<< "$TABLE" [ "$ROWS" -ge 1 ] || fail "scenario table has no data rows" # --- checks 2-4 per row ----------------------------------------------------- i=0 while [ "$i" -lt "$ROWS" ]; do card="${ROW_CARD[$i]}"; falsif="${ROW_FALS[$i]}" f="$CARDS/$card.md" if [ ! -f "$f" ]; then fail "missing card file: $f" i=$((i + 1)); continue fi hay="$(expected_section "$f" | normalize)" case "$hay" in *"$falsif"*) : ;; *) fail "$f: falsification line not present verbatim in the ## Expected section. expected (normalized): $falsif" ;; esac grep -q '\*\*What this covers\*\*' "$f" || fail "$f: missing **What this covers**" for sec in Pre-state Steps Expected Cleanup; do grep -Eiq "^#{2,}[[:space:]]*${sec}[[:space:]]*$" "$f" || fail "$f: missing ## ${sec} section" done i=$((i + 1)) done # --- check 5: extra cards are warnings -------------------------------------- for f in "$CARDS"/*.md; do [ -e "$f" ] || continue base="$(basename "$f" .md)" known=0; i=0 while [ "$i" -lt "$ROWS" ]; do [ "${ROW_CARD[$i]}" = "$base" ] && known=1 i=$((i + 1)) done [ "$known" -eq 1 ] || warn "extra card not in spec table: $base" done if [ "$FAILURES" -gt 0 ]; then echo "$FAILURES check(s) failed" exit 1 fi echo "all checks passed ($ROWS card(s))"