refactor: generalize organized-feedback validation script for any output
This commit is contained in:
@@ -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,136 +7,258 @@ 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() {
|
||||||
echo "FAIL: rg is required but was not found in PATH"
|
cat <<'USAGE'
|
||||||
exit 1
|
Usage:
|
||||||
fi
|
validate_organized_feedback_skill.sh --output <output.md> [--self-check]
|
||||||
|
validate_organized_feedback_skill.sh --help
|
||||||
|
|
||||||
if [[ ! -f "$SKILL_FILE" ]]; then
|
Modes:
|
||||||
echo "FAIL: missing $SKILL_FILE"
|
Default: validate an arbitrary Organized Feedback markdown output file.
|
||||||
exit 1
|
--self-check: additionally validate bundled skill contract and examples.
|
||||||
fi
|
USAGE
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user