diff --git a/skills/organized-feedback/SKILL.md b/skills/organized-feedback/SKILL.md index a218616..aa3b784 100644 --- a/skills/organized-feedback/SKILL.md +++ b/skills/organized-feedback/SKILL.md @@ -81,4 +81,5 @@ Do not write the coverage audit into the final output file. - Preserve the user-specified output path. - Keep the final file limited to the agreed format. - Do not add extra audit notes, scratch work, or intermediate classification logs to the final file. -- If terminal access is available, run `bash skills/organized-feedback/scripts/validate_organized_feedback_skill.sh` after updating this skill to verify contract and example integrity. +- If terminal access is available, validate generated output with `bash skills/organized-feedback/scripts/validate_organized_feedback_skill.sh --output ""`. +- For maintainers, run `bash skills/organized-feedback/scripts/validate_organized_feedback_skill.sh --output "skills/organized-feedback/examples/output-organized-feedback.md" --self-check` to validate both output format and bundled skill contract. diff --git a/skills/organized-feedback/scripts/validate_organized_feedback_skill.sh b/skills/organized-feedback/scripts/validate_organized_feedback_skill.sh index aa7ee64..f4482a6 100755 --- a/skills/organized-feedback/scripts/validate_organized_feedback_skill.sh +++ b/skills/organized-feedback/scripts/validate_organized_feedback_skill.sh @@ -7,136 +7,258 @@ SKILL_FILE="$SKILL_ROOT/SKILL.md" INPUT_EXAMPLE="$SKILL_ROOT/examples/input-pr-review.md" OUTPUT_EXAMPLE="$SKILL_ROOT/examples/output-organized-feedback.md" -if ! command -v rg >/dev/null 2>&1; then - echo "FAIL: rg is required but was not found in PATH" - exit 1 -fi +print_usage() { + cat <<'USAGE' +Usage: + validate_organized_feedback_skill.sh --output [--self-check] + validate_organized_feedback_skill.sh --help -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 - -anchored_patterns=( - '^## Inputs$' - '^## Hard Gates$' - '^## Classification Taxonomy$' - '^## RFC Subfields$' - '^## Processing Flow$' - '^## Interaction-Only Coverage Audit$' - '^## Unknown Handling$' - '^## Final File Format$' - '^## Output Discipline$' - '^- `Change-Scope`: how broad the requested change is\.$' - '^ - `local`: .+$' - '^ - `implement`: .+$' - '^ - `api-change`: .+$' - '^ - `requirement-change`: .+$' - '^- `Necessity`: how strongly the change is required\.$' - '^ - `nice-to-have`: .+$' - '^ - `should-fix`: .+$' - '^ - `must-fix`: .+$' - '^- `All-Source-Refs`: every source reference found in the interaction$' - '^- `Covered-Source-Refs`: refs that are represented in the organized output$' - '^- `Missing-Source-Refs`: refs that are present in the interaction but not yet covered$' - '^Source reference format rule:$' - '^- Use `R` \+ PR numbering path from the source markdown\.$' - '^- Example mapping: `Comment 42\.1\.1` -> `R42\.1\.1`, `Reply 42\.1\.1\.1` -> `R42\.1\.1\.1`\.$' -) - -for pattern in "${anchored_patterns[@]}"; do - if ! rg -n -q --pcre2 "$pattern" "$SKILL_FILE"; then - echo "FAIL: missing required rule -> $pattern" - exit 1 - fi -done - -for pattern in "All refs" "Covered refs" "Missing refs"; do - if rg -Fq "$pattern" "$SKILL_FILE"; then - echo "FAIL: legacy alias found -> $pattern" - exit 1 - fi -done - -for pattern in "output path" "interaction" "coverage audit" "unknown count" "reflection pass" "Unknown Items"; do - if ! rg -Fq -- "$pattern" "$SKILL_FILE"; then - echo "FAIL: missing required rule -> $pattern" - exit 1 - fi -done - -if ! rg -Fq -- "unknown(>=1)" "$SKILL_FILE" || ! rg -Fq -- "反思复判" "$SKILL_FILE"; then - echo "FAIL: missing unknown reflection rule semantics (unknown>=1 + 反思复判)" - exit 1 -fi - -if rg -Fq "Coverage Audit" "$OUTPUT_EXAMPLE"; then - echo "FAIL: coverage audit must not appear in $OUTPUT_EXAMPLE" - exit 1 -fi - -if ! rg -n -q --pcre2 '^## Organized Feedback$' "$OUTPUT_EXAMPLE"; then - echo "FAIL: missing ## Organized Feedback in $OUTPUT_EXAMPLE" - exit 1 -fi - -if ! rg -Fq "Source-Refs:" "$OUTPUT_EXAMPLE"; then - echo "FAIL: missing Source-Refs: line in $OUTPUT_EXAMPLE" - exit 1 -fi - -if ! rg -n -q --pcre2 '^- Source-Refs: R[0-9]+(\.[0-9]+)+(, R[0-9]+(\.[0-9]+)+)*$' "$OUTPUT_EXAMPLE"; then - echo "FAIL: Source-Refs lines must use R-prefixed numeric reference format" - exit 1 -fi - -awk ' - BEGIN { - in_section = 0 - item_count = 0 - item_has_source_refs = 0 - saw_item = 0 - } - /^## Organized Feedback$/ { - in_section = 1 - next - } - in_section && /^## / { - if (saw_item && !item_has_source_refs) { - bad = 1 - } - exit - } - in_section { - if ($0 ~ /^### Item [0-9]+$/) { - if (saw_item && !item_has_source_refs) { - bad = 1 - } - saw_item = 1 - item_has_source_refs = 0 - item_count++ - next - } - if ($0 ~ /^- Source-Refs:/ && saw_item) { - item_has_source_refs = 1 - } - } - END { - if (in_section && saw_item && !item_has_source_refs) { - bad = 1 - } - exit bad - } -' "$OUTPUT_EXAMPLE" || { - echo "FAIL: each ### Item in Organized Feedback must include a Source-Refs line" - exit 1 +Modes: + Default: validate an arbitrary Organized Feedback markdown output file. + --self-check: additionally validate bundled skill contract and examples. +USAGE } -echo "PASS: organized-feedback hard gates validated" +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 "$@"