refactor: generalize organized-feedback validation script for any output

This commit is contained in:
2026-04-09 15:28:26 +08:00
parent 84cb0cf2a9
commit 9cdd943ab9
2 changed files with 254 additions and 131 deletions
+2 -1
View File
@@ -81,4 +81,5 @@ Do not write the coverage audit into the final output file.
- Preserve the user-specified output path. - Preserve the user-specified output path.
- Keep the final file limited to the agreed format. - 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. - 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 "<organized-feedback.md>"`.
- 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.
@@ -7,24 +7,167 @@ SKILL_FILE="$SKILL_ROOT/SKILL.md"
INPUT_EXAMPLE="$SKILL_ROOT/examples/input-pr-review.md" INPUT_EXAMPLE="$SKILL_ROOT/examples/input-pr-review.md"
OUTPUT_EXAMPLE="$SKILL_ROOT/examples/output-organized-feedback.md" OUTPUT_EXAMPLE="$SKILL_ROOT/examples/output-organized-feedback.md"
if ! command -v rg >/dev/null 2>&1; then print_usage() {
cat <<'USAGE'
Usage:
validate_organized_feedback_skill.sh --output <output.md> [--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" echo "FAIL: rg is required but was not found in PATH"
exit 1 exit 1
fi fi
if ! command -v awk >/dev/null 2>&1; then
echo "FAIL: awk is required but was not found in PATH"
exit 1
fi
}
if [[ ! -f "$SKILL_FILE" ]]; then 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" echo "FAIL: missing $SKILL_FILE"
exit 1 exit 1
fi fi
for example_file in "$INPUT_EXAMPLE" "$OUTPUT_EXAMPLE"; do for example_file in "$INPUT_EXAMPLE" "$OUTPUT_EXAMPLE"; do
if [[ ! -f "$example_file" ]]; then if [[ ! -f "$example_file" ]]; then
echo "FAIL: missing $example_file" echo "FAIL: missing $example_file"
exit 1 exit 1
fi fi
done done
anchored_patterns=( local -a anchored_patterns=(
'^## Inputs$' '^## Inputs$'
'^## Hard Gates$' '^## Hard Gates$'
'^## Classification Taxonomy$' '^## Classification Taxonomy$'
@@ -34,109 +177,88 @@ anchored_patterns=(
'^## Unknown Handling$' '^## Unknown Handling$'
'^## Final File Format$' '^## Final File Format$'
'^## Output Discipline$' '^## Output Discipline$'
'^- `Change-Scope`: how broad the requested change is\.$'
'^ - `local`: .+$' '^ - `local`: .+$'
'^ - `implement`: .+$' '^ - `implement`: .+$'
'^ - `api-change`: .+$' '^ - `api-change`: .+$'
'^ - `requirement-change`: .+$' '^ - `requirement-change`: .+$'
'^- `Necessity`: how strongly the change is required\.$'
'^ - `nice-to-have`: .+$' '^ - `nice-to-have`: .+$'
'^ - `should-fix`: .+$' '^ - `should-fix`: .+$'
'^ - `must-fix`: .+$' '^ - `must-fix`: .+$'
'^- `All-Source-Refs`: every source reference found in the interaction$' '^- `All-Source-Refs`: .+$'
'^- `Covered-Source-Refs`: refs that are represented in the organized output$' '^- `Covered-Source-Refs`: .+$'
'^- `Missing-Source-Refs`: refs that are present in the interaction but not yet covered$' '^- `Missing-Source-Refs`: .+$'
'^Source reference format rule:$' '^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 for pattern in "${anchored_patterns[@]}"; do
if ! rg -n -q --pcre2 "$pattern" "$SKILL_FILE"; then if ! rg -n -q --pcre2 "$pattern" "$SKILL_FILE"; then
echo "FAIL: missing required rule -> $pattern" echo "FAIL: missing required skill rule -> $pattern"
exit 1 exit 1
fi fi
done done
for pattern in "All refs" "Covered refs" "Missing refs"; do for legacy in "All refs" "Covered refs" "Missing refs"; do
if rg -Fq "$pattern" "$SKILL_FILE"; then if rg -Fq "$legacy" "$SKILL_FILE"; then
echo "FAIL: legacy alias found -> $pattern" echo "FAIL: legacy alias found in SKILL.md -> $legacy"
exit 1 exit 1
fi fi
done done
for pattern in "output path" "interaction" "coverage audit" "unknown count" "reflection pass" "Unknown Items"; do if ! rg -Fq -- "unknown>=1" "$SKILL_FILE" || ! rg -Fq -- "反思复判" "$SKILL_FILE"; then
if ! rg -Fq -- "$pattern" "$SKILL_FILE"; then echo "FAIL: missing unknown reflection rule semantics in SKILL.md"
echo "FAIL: missing required rule -> $pattern"
exit 1 exit 1
fi fi
done
if ! rg -Fq -- "unknown>=1" "$SKILL_FILE" || ! rg -Fq -- "反思复判" "$SKILL_FILE"; then echo "PASS: skill contract validated -> $SKILL_FILE"
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
} }
echo "PASS: organized-feedback hard gates validated" 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 "$@"