#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SKILL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" SKILL_FILE="$SKILL_ROOT/SKILL.md" INPUT_EXAMPLE="$SKILL_ROOT/examples/input-pr-review.md" OUTPUT_EXAMPLE="$SKILL_ROOT/examples/output-organized-feedback.md" print_usage() { cat <<'USAGE' Usage: validate_organized_feedback_skill.sh --output [--self-check] validate_organized_feedback_skill.sh --help Modes: Default: validate an arbitrary Organized Feedback markdown output file. --self-check: additionally validate bundled skill contract and examples. USAGE } require_cmds() { if ! command -v rg >/dev/null 2>&1; then echo "FAIL: rg is required but was not found in PATH" exit 1 fi if ! command -v awk >/dev/null 2>&1; then echo "FAIL: awk is required but was not found in PATH" exit 1 fi } validate_output_file() { local file="$1" if [[ ! -f "$file" ]]; then echo "FAIL: missing output file -> $file" exit 1 fi if ! rg -n -q --pcre2 '^## Organized Feedback$' "$file"; then echo "FAIL: missing required section -> ## Organized Feedback" exit 1 fi if rg -n -q -i --pcre2 'coverage[ -]?audit' "$file"; then echo "FAIL: output must not include Coverage Audit" exit 1 fi awk ' function trim(s) { gsub(/^[ \t]+/, "", s) gsub(/[ \t]+$/, "", s) return s } function validate_refs(line) { sub(/^- Source-Refs:[ \t]*/, "", line) n = split(line, parts, /,/) if (n < 1) return 0 for (i = 1; i <= n; i++) { ref = trim(parts[i]) if (ref !~ /^R[0-9]+(\.[0-9]+)+$/) { return 0 } } return 1 } function reset_item() { item_seen = 0 has_type = 0 has_refs = 0 is_rfc = 0 has_scope = 0 has_necessity = 0 } function finalize_item() { if (!item_seen) return 1 if (!has_type) { print "FAIL: item missing - Type: line" > "/dev/stderr" return 0 } if (!has_refs) { print "FAIL: item missing valid - Source-Refs: line" > "/dev/stderr" return 0 } if (is_rfc && (!has_scope || !has_necessity)) { print "FAIL: request-for-change item must include Change-Scope and Necessity" > "/dev/stderr" return 0 } return 1 } BEGIN { in_of = 0 item_count = 0 reset_item() } /^## Organized Feedback$/ { in_of = 1 next } in_of && /^## / { if (!finalize_item()) exit 1 in_of = 0 next } in_of { if ($0 ~ /^### Item [0-9]+$/) { if (!finalize_item()) exit 1 reset_item() item_seen = 1 item_count++ next } if (!item_seen) next if ($0 ~ /^- Type:[ \t]*/) { has_type = 1 if ($0 ~ /^- Type:[ \t]*request-for-change([ \t]|$)/) { is_rfc = 1 } } if ($0 ~ /^- Change-Scope:[ \t]*/) has_scope = 1 if ($0 ~ /^- Necessity:[ \t]*/) has_necessity = 1 if ($0 ~ /^- Source-Refs:[ \t]*/) { if (validate_refs($0)) { has_refs = 1 } else { print "FAIL: invalid Source-Refs format -> " $0 > "/dev/stderr" exit 1 } } } END { if (in_of && !finalize_item()) exit 1 if (item_count == 0) { print "FAIL: ## Organized Feedback has no ### Item entries" > "/dev/stderr" exit 1 } } ' "$file" echo "PASS: output format validated -> $file" } validate_skill_contract() { if [[ ! -f "$SKILL_FILE" ]]; then echo "FAIL: missing $SKILL_FILE" exit 1 fi for example_file in "$INPUT_EXAMPLE" "$OUTPUT_EXAMPLE"; do if [[ ! -f "$example_file" ]]; then echo "FAIL: missing $example_file" exit 1 fi done local -a anchored_patterns=( '^## Inputs$' '^## Hard Gates$' '^## Classification Taxonomy$' '^## RFC Subfields$' '^## Processing Flow$' '^## Interaction-Only Coverage Audit$' '^## Unknown Handling$' '^## Final File Format$' '^## Output Discipline$' '^ - `local`: .+$' '^ - `implement`: .+$' '^ - `api-change`: .+$' '^ - `requirement-change`: .+$' '^ - `nice-to-have`: .+$' '^ - `should-fix`: .+$' '^ - `must-fix`: .+$' '^- `All-Source-Refs`: .+$' '^- `Covered-Source-Refs`: .+$' '^- `Missing-Source-Refs`: .+$' '^Source reference format rule:$' ) for pattern in "${anchored_patterns[@]}"; do if ! rg -n -q --pcre2 "$pattern" "$SKILL_FILE"; then echo "FAIL: missing required skill rule -> $pattern" exit 1 fi done for legacy in "All refs" "Covered refs" "Missing refs"; do if rg -Fq "$legacy" "$SKILL_FILE"; then echo "FAIL: legacy alias found in SKILL.md -> $legacy" exit 1 fi done if ! rg -Fq -- "unknown(>=1)" "$SKILL_FILE" || ! rg -Fq -- "反思复判" "$SKILL_FILE"; then echo "FAIL: missing unknown reflection rule semantics in SKILL.md" exit 1 fi echo "PASS: skill contract validated -> $SKILL_FILE" } main() { require_cmds local output_file="" local self_check=0 while [[ $# -gt 0 ]]; do case "$1" in --output) if [[ $# -lt 2 ]]; then echo "FAIL: --output requires a file path" print_usage exit 1 fi output_file="$2" shift 2 ;; --self-check) self_check=1 shift ;; --help|-h) print_usage exit 0 ;; *) echo "FAIL: unknown argument -> $1" print_usage exit 1 ;; esac done if [[ -z "$output_file" ]]; then echo "FAIL: --output is required" print_usage exit 1 fi validate_output_file "$output_file" if [[ "$self_check" -eq 1 ]]; then validate_skill_contract validate_output_file "$OUTPUT_EXAMPLE" fi echo "PASS: organized-feedback validation completed" } main "$@"