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.
- 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 "<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,11 +7,154 @@ 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 <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"
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
@@ -24,7 +167,7 @@ for example_file in "$INPUT_EXAMPLE" "$OUTPUT_EXAMPLE"; do
fi
done
anchored_patterns=(
local -a anchored_patterns=(
'^## Inputs$'
'^## Hard Gates$'
'^## Classification Taxonomy$'
@@ -34,109 +177,88 @@ anchored_patterns=(
'^## 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$'
'^- `All-Source-Refs`: .+$'
'^- `Covered-Source-Refs`: .+$'
'^- `Missing-Source-Refs`: .+$'
'^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"
echo "FAIL: missing required skill 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"
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 (unknown>=1 + 反思复判)"
echo "FAIL: missing unknown reflection rule semantics in SKILL.md"
exit 1
fi
if rg -Fq "Coverage Audit" "$OUTPUT_EXAMPLE"; then
echo "FAIL: coverage audit must not appear in $OUTPUT_EXAMPLE"
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
if ! rg -n -q --pcre2 '^## Organized Feedback$' "$OUTPUT_EXAMPLE"; then
echo "FAIL: missing ## Organized Feedback in $OUTPUT_EXAMPLE"
exit 1
validate_output_file "$output_file"
if [[ "$self_check" -eq 1 ]]; then
validate_skill_contract
validate_output_file "$OUTPUT_EXAMPLE"
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 validation completed"
}
echo "PASS: organized-feedback hard gates validated"
main "$@"