-
Notifications
You must be signed in to change notification settings - Fork 2
feat(scripts): add consumer-side handoff queue front-matter validator #2444
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| #!/usr/bin/env bash | ||
| # Consumer-side front-matter validator for file-based markdown handoff queues | ||
| # (one item per file with YAML front matter). Detects malformed items that make | ||
| # a `grep '^status:'` reconciliation report a false all-clear (#1647). | ||
| # | ||
| # scripts/check-queue-front-matter.sh <queue-dir> | ||
| # | ||
| # Not a CI gate — the queue lives outside the repository. Invoke at claim time | ||
| # when an agent decides whether work is present. | ||
| # | ||
| # Exit 0 = every item file conforms; 1 = one or more violations; 2 = usage error. | ||
| set -euo pipefail | ||
|
|
||
| usage() { | ||
| printf 'usage: check-queue-front-matter.sh <queue-dir>\n' >&2 | ||
| exit 2 | ||
| } | ||
|
|
||
| [[ $# -eq 1 ]] || usage | ||
| QUEUE_DIR="$1" | ||
|
|
||
| if [[ ! -d "$QUEUE_DIR" ]]; then | ||
| printf 'Error: queue directory not found: %s\n' "$QUEUE_DIR" >&2 | ||
| exit 2 | ||
| fi | ||
|
|
||
| VALID_STATUSES='unclaimed|claimed|in-progress|blocked|done' | ||
| VALID_PRIORITIES='low|medium|high|urgent' | ||
| REQUIRED_KEYS=(id title status created producer) | ||
|
|
||
| errors=0 | ||
| file_count=0 | ||
| parsed_count=0 | ||
|
|
||
| report_violation() { | ||
| printf 'VIOLATION: %s — %s\n' "$1" "$2" | ||
| errors=$((errors + 1)) | ||
| } | ||
|
|
||
| # extract_front_matter <file> — prints front matter body or nothing. | ||
| extract_front_matter() { | ||
| awk ' | ||
| NR == 1 && $0 == "---" { in_fm = 1; next } | ||
|
kyle-sexton marked this conversation as resolved.
|
||
| in_fm && $0 == "---" { exit } | ||
| in_fm { print } | ||
|
kyle-sexton marked this conversation as resolved.
|
||
| ' "$1" | ||
| } | ||
|
|
||
| # fm_value <front_matter> <key> | ||
| fm_value() { | ||
| awk -v key="$2" ' | ||
| $1 == key ":" { | ||
| sub(/^[^:]*:[[:space:]]*/, "") | ||
| exit | ||
| } | ||
| ' <<<"$1" | ||
| } | ||
|
Comment on lines
+50
to
+58
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Important — trailing whitespace on a value causes false-positive violations.
That trailing whitespace survives into every comparison that isn't itself whitespace-tolerant:
The "missing required key" check at line 75 happens to be immune (it strips all whitespace before testing emptiness), which is likely why this wasn't caught by hand-testing. A one-line fix in |
||
|
|
||
| for item in "$QUEUE_DIR"/*.md; do | ||
| [[ -e "$item" ]] || continue | ||
| base="$(basename "$item")" | ||
| [[ "$base" == README.md ]] && continue | ||
| file_count=$((file_count + 1)) | ||
| stem="${base%.md}" | ||
| fm="$(extract_front_matter "$item")" | ||
| if [[ -z "${fm//[[:space:]]/}" ]]; then | ||
| report_violation "$item" 'missing or empty YAML front matter' | ||
| continue | ||
| fi | ||
| parsed_count=$((parsed_count + 1)) | ||
|
|
||
| for key in "${REQUIRED_KEYS[@]}"; do | ||
| val="$(fm_value "$fm" "$key")" | ||
| if [[ -z "${val//[[:space:]]/}" ]]; then | ||
| report_violation "$item" "missing required key: $key" | ||
| fi | ||
| done | ||
|
|
||
| status="$(fm_value "$fm" status)" | ||
| if [[ -n "$status" ]]; then | ||
| if ! grep -qE "^(${VALID_STATUSES})$" <<<"$status"; then | ||
| report_violation "$item" "status '$status' not in documented set (unclaimed|claimed|in-progress|blocked|done)" | ||
| fi | ||
| fi | ||
|
|
||
| priority="$(fm_value "$fm" priority)" | ||
| if [[ -n "${priority//[[:space:]]/}" ]]; then | ||
| if ! grep -qE "^(${VALID_PRIORITIES})$" <<<"$priority"; then | ||
| report_violation "$item" "priority '$priority' not in documented set (low|medium|high|urgent)" | ||
| fi | ||
| fi | ||
|
|
||
| id="$(fm_value "$fm" id)" | ||
| if [[ -n "$id" && "$id" != "$stem" ]]; then | ||
| report_violation "$item" "id '$id' does not match filename stem '$stem'" | ||
| fi | ||
| done | ||
|
|
||
| printf 'Reconciliation: %d item file(s), %d with parseable front matter\n' \ | ||
| "$file_count" "$parsed_count" | ||
|
|
||
| if ((file_count != parsed_count)); then | ||
| report_violation "$QUEUE_DIR" \ | ||
| "count gap — $((file_count - parsed_count)) file(s) lack parseable front matter (never reconcile by status grep alone)" | ||
| fi | ||
|
|
||
| if ((errors > 0)); then | ||
| printf '\n%d violation(s).\n' "$errors" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "Queue front matter OK." | ||
| exit 0 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| #!/usr/bin/env bash | ||
| # Unit tests for check-queue-front-matter.sh. | ||
| set -uo pipefail | ||
|
|
||
| SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| SCRIPT="$SELF_DIR/check-queue-front-matter.sh" | ||
|
Comment on lines
+1
to
+6
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Important — this test suite doesn't appear to run anywhere.
The PR's decision brief says the checker itself is intentionally "not wired to CI" because the queue lives outside the repo — but that reasoning doesn't extend to the unit tests: they're fully self-contained (each test does its own |
||
|
|
||
| PASS=0 | ||
| FAIL=0 | ||
| fail() { | ||
| echo "FAIL: $*" >&2 | ||
| FAIL=$((FAIL + 1)) | ||
| } | ||
| ok() { | ||
| echo "ok: $*" | ||
| PASS=$((PASS + 1)) | ||
| } | ||
|
|
||
| new_queue() { | ||
| mktemp -d | ||
| } | ||
|
|
||
| run_check() ( | ||
| bash "$SCRIPT" "$1" | ||
| ) | ||
|
|
||
| # --- valid item passes ------------------------------------------------------- | ||
| q="$(new_queue)" | ||
| cat >"$q/20260812-sample.md" <<'EOF' | ||
| --- | ||
| id: 20260812-sample | ||
| title: Sample item | ||
| status: unclaimed | ||
| created: 2026-08-12T12:00:00Z | ||
| producer: test-fixture | ||
| --- | ||
| Body | ||
| EOF | ||
| if run_check "$q" >/dev/null 2>&1; then | ||
| ok "valid item passes" | ||
| else | ||
| fail "valid item should pass" | ||
| fi | ||
|
|
||
| # --- missing front matter fails ---------------------------------------------- | ||
| q="$(new_queue)" | ||
| printf 'No front matter here\n' >"$q/20260812-bad.md" | ||
| if run_check "$q" >/dev/null 2>&1; then | ||
| fail "missing front matter should fail" | ||
| else | ||
| ok "missing front matter fails" | ||
| fi | ||
|
|
||
| # --- invalid status fails ---------------------------------------------------- | ||
| q="$(new_queue)" | ||
| cat >"$q/20260812-open.md" <<'EOF' | ||
| --- | ||
| id: 20260812-open | ||
| title: Bad status | ||
| status: open | ||
| created: 2026-08-12T12:00:00Z | ||
| producer: test-fixture | ||
| --- | ||
| EOF | ||
| if run_check "$q" >/dev/null 2>&1; then | ||
| fail "invalid status should fail" | ||
| else | ||
| ok "invalid status fails" | ||
| fi | ||
|
|
||
| # --- id stem mismatch fails -------------------------------------------------- | ||
| q="$(new_queue)" | ||
| cat >"$q/20260812-wrong.md" <<'EOF' | ||
| --- | ||
| id: other-id | ||
| title: Mismatch | ||
| status: unclaimed | ||
| created: 2026-08-12T12:00:00Z | ||
| producer: test-fixture | ||
| --- | ||
| EOF | ||
| if run_check "$q" >/dev/null 2>&1; then | ||
| fail "id/filename mismatch should fail" | ||
| else | ||
| ok "id stem mismatch fails" | ||
| fi | ||
|
|
||
| # --- README.md is ignored ---------------------------------------------------- | ||
| q="$(new_queue)" | ||
| printf '# readme\n' >"$q/README.md" | ||
| cat >"$q/20260812-only.md" <<'EOF' | ||
| --- | ||
| id: 20260812-only | ||
| title: Only item | ||
| status: done | ||
| created: 2026-08-12T12:00:00Z | ||
| producer: test-fixture | ||
| --- | ||
| EOF | ||
| if run_check "$q" >/dev/null 2>&1; then | ||
| ok "README.md ignored" | ||
| else | ||
| fail "README should be ignored: valid sole item should pass" | ||
| fi | ||
|
|
||
| printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" | ||
| [[ "$FAIL" -eq 0 ]] | ||
|
Comment on lines
+100
to
+107
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion — coverage gaps relative to the checker's own logic. The five cases here (valid item, wholesale-missing front matter, bad status, id/stem mismatch, README skip) all just assert pass/fail via exit status. A few things the checker does aren't exercised by any case:
None of these are required for the PR to be correct, but since this suite won't run in CI yet (see the other comment on this file) and the checker's core value proposition is exactly the reconciliation-count behavior, it'd be worth locking that down with a test before it can silently regress. |
||
Uh oh!
There was an error while loading. Please reload this page.