Files
boshu2__agentops/scripts/check-loop-shape.sh
T

277 lines
9.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# practices: [bdd-gherkin, ddd-bounded-context, tdd]
# check-loop-shape.sh — warn-only loop-shape gate for non-trivial beads.
#
# Implements the initial gate declared by GOALS.md Directive 12: beads tagged
# `non-trivial` should expose loop shape before implementation — exactly one
# bounded context label, at least one Gherkin block (Given/When/Then), at least
# one slice candidate, and one first-failing proof. This is the
# BDD/Gherkin/DDD/XP operating loop's mechanical warning.
#
# Posture: WARN-ONLY. The script exits 0 even when beads are missing loop
# shape, so it never blocks a push. It flips to blocking only when invoked
# with --strict (or AGENTOPS_LOOP_SHAPE_STRICT=1), which Directive 12 reserves
# for once the corpus-wide pass rate is stable.
#
# Usage:
# check-loop-shape.sh # inspect live `bd` open + in_progress beads
# check-loop-shape.sh --json FILE # inspect a bd-JSON array from FILE
# check-loop-shape.sh --strict # exit 1 if any non-trivial bead lacks shape
# check-loop-shape.sh --self-test # run built-in fixtures and assert behavior
# check-loop-shape.sh --help
#
# Exit codes:
# 0 = warn-only run (always), or strict run with no offenders, or self-test pass
# 1 = strict run with at least one offender, or self-test failure
# 2 = script error (bad invocation, missing dependency)
set -uo pipefail
STRICT="${AGENTOPS_LOOP_SHAPE_STRICT:-0}"
BD_TIMEOUT_SECONDS="${AGENTOPS_LOOP_SHAPE_BD_TIMEOUT_SECONDS:-5}"
JSON_FILE=""
SELF_TEST=0
BD_TIMED_OUT=0
while [ $# -gt 0 ]; do
case "$1" in
--strict) STRICT=1 ;;
--self-test) SELF_TEST=1 ;;
--json)
JSON_FILE="${2:-}"
if [ -z "$JSON_FILE" ]; then
echo "check-loop-shape: --json needs a file argument" >&2
exit 2
fi
shift
;;
-h|--help)
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
echo "check-loop-shape: unknown argument: $1" >&2
exit 2
;;
esac
shift
done
if ! command -v jq >/dev/null 2>&1; then
echo "check-loop-shape: SKIP (jq not available)"
exit 0
fi
# analyze_beads <json-string>
# Emits one "WARN: ..." line per non-trivial bead missing loop shape on stdout,
# then a final "OFFENDERS: <n>" line. Returns 0 always; callers read the count.
analyze_beads() {
local json="$1"
local rows
rows=$(printf '%s' "$json" | jq -r '
def evidence_text:
[(.description // ""), (.acceptance_criteria // ""), (.notes // "")]
| join("\n");
def context_labels:
[(.labels // [])[]
| select(test("^bc-(corpus|validation|loop|factory|runtime)$"))]
| unique;
[ .[]
| select((.labels // []) | index("non-trivial"))
| { id: .id,
gherkin: ((evidence_text | test("\\bGiven\\b") and test("\\bWhen\\b") and test("\\bThen\\b"))),
slice: ((evidence_text | test("(?i)\\bslice(s| candidates)?\\b") or test("\\bS[0-9]+\\b"))),
proof: ((evidence_text | test("(?i)first[ _-]*failing[ _-]*(proof|test)"))),
contexts: context_labels }
] | .[] | [.id, (.gherkin|tostring), (.slice|tostring), (.proof|tostring), ((.contexts|length)|tostring), (.contexts|join(","))] | @tsv
' 2>/dev/null)
local offenders=0
if [ -n "$rows" ]; then
while IFS=$'\t' read -r id gherkin slice proof context_count contexts; do
[ -z "$id" ] && continue
local missing=""
if [ "$context_count" != "1" ]; then
missing="bounded context label (exactly one of bc-corpus|bc-validation|bc-loop|bc-factory|bc-runtime"
if [ -n "$contexts" ]; then
missing="$missing; found $contexts"
fi
missing="$missing)"
fi
if [ "$gherkin" != "true" ]; then
[ -n "$missing" ] && missing="$missing and "
missing="${missing}Gherkin block (Given/When/Then)"
fi
if [ "$slice" != "true" ]; then
[ -n "$missing" ] && missing="$missing and "
missing="${missing}slice candidate"
fi
if [ "$proof" != "true" ]; then
[ -n "$missing" ] && missing="$missing and "
missing="${missing}first failing proof"
fi
if [ -n "$missing" ]; then
echo "WARN: $id — non-trivial bead is missing: $missing"
offenders=$((offenders + 1))
fi
done <<< "$rows"
fi
echo "OFFENDERS: $offenders"
}
bd_list_with_timeout() {
local status="$1"
local tmp
tmp=$(mktemp "${TMPDIR:-/tmp}/agentops-loop-shape.XXXXXX") || {
echo '[]'
return 0
}
bd list --status="$status" --json >"$tmp" 2>/dev/null &
local pid=$!
local waited=0
while kill -0 "$pid" 2>/dev/null; do
if [ "$waited" -ge "$BD_TIMEOUT_SECONDS" ]; then
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
rm -f "$tmp"
echo '[]'
return 124
fi
sleep 1
waited=$((waited + 1))
done
if wait "$pid"; then
cat "$tmp"
else
echo '[]'
fi
rm -f "$tmp"
}
self_test() {
local fixture
fixture=$(cat <<'EOF'
[
{ "id": "fix-good", "labels": ["non-trivial", "bc-loop", "xp"],
"description": "Feature: x\n Scenario: y\n Given a\n When b\n Then c\nSlice candidates: S1 do the thing\nFirst failing proof: go test ./..." },
{ "id": "fix-split-fields", "labels": ["non-trivial", "bc-corpus"],
"description": "Problem statement only.",
"acceptance_criteria": "Slice candidates: S1 do the thing\nFirst failing proof: go test ./...",
"notes": "Feature: x\n Scenario: y\n Given a\n When b\n Then c" },
{ "id": "fix-nogherkin", "labels": ["non-trivial", "bc-loop"],
"description": "Just do the work. Slice S1 covers it. First failing proof: go test ./..." },
{ "id": "fix-noslice", "labels": ["non-trivial", "bc-loop"],
"description": "Given a\n When b\n Then c\nFirst failing proof: go test ./..." },
{ "id": "fix-nocontext", "labels": ["non-trivial"],
"description": "Given a\nWhen b\nThen c\nSlice candidates: S1\nFirst failing proof: go test ./..." },
{ "id": "fix-multicontext", "labels": ["non-trivial", "bc-loop", "bc-corpus"],
"description": "Given a\nWhen b\nThen c\nSlice candidates: S1\nFirst failing proof: go test ./..." },
{ "id": "fix-noproof", "labels": ["non-trivial", "bc-loop"],
"description": "Given a\nWhen b\nThen c\nSlice candidates: S1" },
{ "id": "fix-trivial", "labels": ["chore"],
"description": "rename a variable" }
]
EOF
)
local out
out=$(analyze_beads "$fixture")
local count
count=$(printf '%s\n' "$out" | sed -n 's/^OFFENDERS: //p')
local fails=0
if [ "$count" != "5" ]; then
echo "SELF-TEST FAIL: expected 5 offenders, got '$count'" >&2
fails=$((fails + 1))
fi
if ! printf '%s\n' "$out" | grep -q "^WARN: fix-nogherkin .*Gherkin block"; then
echo "SELF-TEST FAIL: fix-nogherkin should warn about a missing Gherkin block" >&2
fails=$((fails + 1))
fi
if ! printf '%s\n' "$out" | grep -q "^WARN: fix-noslice .*slice candidate"; then
echo "SELF-TEST FAIL: fix-noslice should warn about a missing slice candidate" >&2
fails=$((fails + 1))
fi
if ! printf '%s\n' "$out" | grep -q "^WARN: fix-nocontext .*bounded context label"; then
echo "SELF-TEST FAIL: fix-nocontext should warn about a missing bounded context label" >&2
fails=$((fails + 1))
fi
if ! printf '%s\n' "$out" | grep -q "^WARN: fix-multicontext .*bounded context label"; then
echo "SELF-TEST FAIL: fix-multicontext should warn about multiple bounded context labels" >&2
fails=$((fails + 1))
fi
if ! printf '%s\n' "$out" | grep -q "^WARN: fix-noproof .*first failing proof"; then
echo "SELF-TEST FAIL: fix-noproof should warn about a missing first failing proof" >&2
fails=$((fails + 1))
fi
if printf '%s\n' "$out" | grep -q "^WARN: fix-good"; then
echo "SELF-TEST FAIL: fix-good has full loop shape and must not warn" >&2
fails=$((fails + 1))
fi
if printf '%s\n' "$out" | grep -q "^WARN: fix-split-fields"; then
echo "SELF-TEST FAIL: fix-split-fields has evidence across bd fields and must not warn" >&2
fails=$((fails + 1))
fi
if printf '%s\n' "$out" | grep -q "^WARN: fix-trivial"; then
echo "SELF-TEST FAIL: fix-trivial is not labeled non-trivial and must be ignored" >&2
fails=$((fails + 1))
fi
if [ "$fails" -ne 0 ]; then
echo "check-loop-shape: self-test FAILED ($fails assertion(s))" >&2
return 1
fi
echo "check-loop-shape: self-test PASS"
return 0
}
if [ "$SELF_TEST" -eq 1 ]; then
self_test
exit $?
fi
# Resolve the bead JSON: explicit fixture file, or live bd.
if [ -n "$JSON_FILE" ]; then
if [ ! -f "$JSON_FILE" ]; then
echo "check-loop-shape: --json file not found: $JSON_FILE" >&2
exit 2
fi
BEAD_JSON=$(cat "$JSON_FILE")
else
if ! command -v bd >/dev/null 2>&1; then
echo "check-loop-shape: SKIP (bd not available; pass --json FILE to inspect a fixture)"
exit 0
fi
OPEN_JSON=$(bd_list_with_timeout open) || BD_TIMED_OUT=1
PROG_JSON=$(bd_list_with_timeout in_progress) || BD_TIMED_OUT=1
if [ "$BD_TIMED_OUT" -eq 1 ]; then
echo "check-loop-shape: SKIP (bd list timed out after ${BD_TIMEOUT_SECONDS}s; pass --json FILE for deterministic validation)"
exit 0
fi
BEAD_JSON=$(printf '%s\n%s' "$OPEN_JSON" "$PROG_JSON" | jq -s 'add // []' 2>/dev/null || echo '[]')
fi
RESULT=$(analyze_beads "$BEAD_JSON")
WARNINGS=$(printf '%s\n' "$RESULT" | grep '^WARN: ' || true)
OFFENDERS=$(printf '%s\n' "$RESULT" | sed -n 's/^OFFENDERS: //p')
OFFENDERS="${OFFENDERS:-0}"
if [ -n "$WARNINGS" ]; then
printf '%s\n' "$WARNINGS"
fi
if [ "$OFFENDERS" -eq 0 ]; then
echo "check-loop-shape: PASS (no non-trivial beads missing loop shape)"
exit 0
fi
if [ "$STRICT" = "1" ]; then
echo "check-loop-shape: FAIL — $OFFENDERS non-trivial bead(s) missing loop shape (--strict)"
exit 1
fi
echo "check-loop-shape: WARN-ONLY — $OFFENDERS non-trivial bead(s) missing loop shape (Directive 12 posture; --strict to block)"
exit 0