You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
226 lines
6.7 KiB
226 lines
6.7 KiB
|
2 weeks ago
|
#!/bin/bash
|
||
|
|
# Comet YAML Schema Validator — validates .comet.yaml structure
|
||
|
|
# Usage: comet-yaml-validate.sh <change-name>
|
||
|
|
# Exit 0 = valid, exit 1 = errors found (printed to stderr)
|
||
|
|
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
red() { echo -e "\033[31m$1\033[0m" >&2; }
|
||
|
|
green() { echo -e "\033[32m$1\033[0m" >&2; }
|
||
|
|
warn() { echo -e "\033[33m$1\033[0m" >&2; }
|
||
|
|
|
||
|
|
# Input validation - prevent path traversal
|
||
|
|
validate_change_name() {
|
||
|
|
local name="$1"
|
||
|
|
# Reject empty names
|
||
|
|
if [ -z "$name" ]; then
|
||
|
|
red "ERROR: Change name cannot be empty" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
# Only allow alphanumeric, hyphens, and underscores
|
||
|
|
if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
||
|
|
red "ERROR: Invalid change name: '$name'" >&2
|
||
|
|
red "Valid characters: a-z, A-Z, 0-9, -, _" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
# Reject path traversal attempts
|
||
|
|
if [[ "$name" =~ \.\. ]]; then
|
||
|
|
red "ERROR: Change name cannot contain '..' (path traversal not allowed)" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
validate_change_name "$1"
|
||
|
|
|
||
|
|
CHANGE="$1"
|
||
|
|
CHANGE_DIR="openspec/changes/$CHANGE"
|
||
|
|
if [ ! -d "$CHANGE_DIR" ] && [ -d "openspec/changes/archive/$CHANGE" ]; then
|
||
|
|
CHANGE_DIR="openspec/changes/archive/$CHANGE"
|
||
|
|
fi
|
||
|
|
YAML="$CHANGE_DIR/.comet.yaml"
|
||
|
|
|
||
|
|
ERRORS=0
|
||
|
|
WARNINGS=0
|
||
|
|
|
||
|
|
# Helper: get value of a top-level field (handles null, empty, quoted)
|
||
|
|
field_value() {
|
||
|
|
local value
|
||
|
|
value=$(grep "^${1}:" "$YAML" 2>/dev/null | sed "s/^${1}: *//" || true)
|
||
|
|
value=$(strip_inline_comment "$value")
|
||
|
|
strip_wrapping_quotes "$value"
|
||
|
|
}
|
||
|
|
|
||
|
|
strip_inline_comment() {
|
||
|
|
local value="$1"
|
||
|
|
printf '%s\n' "$value" | awk -v squote="'" '
|
||
|
|
{
|
||
|
|
out = ""
|
||
|
|
quote = ""
|
||
|
|
for (i = 1; i <= length($0); i++) {
|
||
|
|
c = substr($0, i, 1)
|
||
|
|
if (quote == "") {
|
||
|
|
if (c == "\"" || c == squote) {
|
||
|
|
quote = c
|
||
|
|
} else if (c == "#" && (i == 1 || substr($0, i - 1, 1) ~ /[[:space:]]/)) {
|
||
|
|
sub(/[[:space:]]+$/, "", out)
|
||
|
|
print out
|
||
|
|
next
|
||
|
|
}
|
||
|
|
} else if (c == quote) {
|
||
|
|
quote = ""
|
||
|
|
}
|
||
|
|
out = out c
|
||
|
|
}
|
||
|
|
print out
|
||
|
|
}
|
||
|
|
'
|
||
|
|
}
|
||
|
|
|
||
|
|
strip_wrapping_quotes() {
|
||
|
|
local value="$1"
|
||
|
|
case "$value" in
|
||
|
|
\"*\")
|
||
|
|
printf '%s\n' "${value:1:${#value}-2}"
|
||
|
|
;;
|
||
|
|
\'*\')
|
||
|
|
printf '%s\n' "${value:1:${#value}-2}"
|
||
|
|
;;
|
||
|
|
*)
|
||
|
|
printf '%s\n' "$value"
|
||
|
|
;;
|
||
|
|
esac
|
||
|
|
}
|
||
|
|
|
||
|
|
fail() { red " FAIL: $1"; ERRORS=$((ERRORS + 1)); }
|
||
|
|
warn_msg() { warn " WARN: $1"; WARNINGS=$((WARNINGS + 1)); }
|
||
|
|
|
||
|
|
echo "[VALIDATE] $YAML" >&2
|
||
|
|
|
||
|
|
# --- Required fields ---
|
||
|
|
REQUIRED_FIELDS="workflow phase design_doc plan build_mode isolation verify_mode verify_result verified_at archived"
|
||
|
|
for field in $REQUIRED_FIELDS; do
|
||
|
|
if ! grep -q "^${field}:" "$YAML" 2>/dev/null; then
|
||
|
|
fail "missing required field '$field'"
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
|
||
|
|
# --- Enum validation ---
|
||
|
|
validate_enum() {
|
||
|
|
local field="$1" value="$2"
|
||
|
|
shift 2
|
||
|
|
local valid_values="$*"
|
||
|
|
|
||
|
|
# null or empty is always acceptable
|
||
|
|
if [ -z "$value" ] || [ "$value" = "null" ]; then
|
||
|
|
return 0
|
||
|
|
fi
|
||
|
|
|
||
|
|
for v in $valid_values; do
|
||
|
|
if [ "$value" = "$v" ]; then
|
||
|
|
return 0
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
fail "$field='$value' is not valid. Expected: $valid_values"
|
||
|
|
}
|
||
|
|
|
||
|
|
validate_required_enum() {
|
||
|
|
local field="$1" value="$2"
|
||
|
|
shift 2
|
||
|
|
local valid_values="$*"
|
||
|
|
|
||
|
|
if [ -z "$value" ] || [ "$value" = "null" ]; then
|
||
|
|
fail "$field='${value:-}' is not valid. Expected: $valid_values"
|
||
|
|
return 0
|
||
|
|
fi
|
||
|
|
|
||
|
|
validate_enum "$field" "$value" "$@"
|
||
|
|
}
|
||
|
|
|
||
|
|
workflow=$(field_value "workflow")
|
||
|
|
phase=$(field_value "phase")
|
||
|
|
context_compression=$(field_value "context_compression")
|
||
|
|
build_mode=$(field_value "build_mode")
|
||
|
|
build_pause=$(field_value "build_pause")
|
||
|
|
subagent_dispatch=$(field_value "subagent_dispatch")
|
||
|
|
tdd_mode=$(field_value "tdd_mode")
|
||
|
|
review_mode=$(field_value "review_mode")
|
||
|
|
isolation=$(field_value "isolation")
|
||
|
|
verify_mode=$(field_value "verify_mode")
|
||
|
|
auto_transition=$(field_value "auto_transition")
|
||
|
|
verify_result=$(field_value "verify_result")
|
||
|
|
branch_status=$(field_value "branch_status")
|
||
|
|
archived=$(field_value "archived")
|
||
|
|
direct_override=$(field_value "direct_override")
|
||
|
|
design_doc=$(field_value "design_doc")
|
||
|
|
plan=$(field_value "plan")
|
||
|
|
handoff_context=$(field_value "handoff_context")
|
||
|
|
handoff_hash=$(field_value "handoff_hash")
|
||
|
|
|
||
|
|
validate_enum "workflow" "$workflow" "full hotfix tweak"
|
||
|
|
validate_enum "phase" "$phase" "open design build verify archive"
|
||
|
|
validate_enum "context_compression" "$context_compression" "off beta"
|
||
|
|
validate_enum "build_mode" "$build_mode" "subagent-driven-development executing-plans direct"
|
||
|
|
validate_enum "build_pause" "$build_pause" "null plan-ready"
|
||
|
|
validate_enum "subagent_dispatch" "$subagent_dispatch" "null confirmed"
|
||
|
|
validate_enum "tdd_mode" "$tdd_mode" "tdd direct null"
|
||
|
|
validate_enum "review_mode" "$review_mode" "off standard thorough"
|
||
|
|
validate_enum "isolation" "$isolation" "branch worktree"
|
||
|
|
validate_enum "verify_mode" "$verify_mode" "light full"
|
||
|
|
if grep -q "^auto_transition:" "$YAML" 2>/dev/null; then
|
||
|
|
validate_required_enum "auto_transition" "$auto_transition" "true false"
|
||
|
|
fi
|
||
|
|
validate_enum "verify_result" "$verify_result" "pending pass fail"
|
||
|
|
validate_enum "branch_status" "$branch_status" "pending handled"
|
||
|
|
validate_enum "archived" "$archived" "true false"
|
||
|
|
validate_enum "direct_override" "$direct_override" "true false"
|
||
|
|
|
||
|
|
# --- Path validation ---
|
||
|
|
|
||
|
|
if [ -n "$design_doc" ] && [ "$design_doc" != "null" ]; then
|
||
|
|
if [ ! -f "$design_doc" ]; then
|
||
|
|
fail "design_doc='$design_doc' does not exist on disk"
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ -n "$plan" ] && [ "$plan" != "null" ]; then
|
||
|
|
if [ ! -f "$plan" ]; then
|
||
|
|
fail "plan='$plan' does not exist on disk"
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ -n "$handoff_context" ] && [ "$handoff_context" != "null" ]; then
|
||
|
|
if [ ! -f "$handoff_context" ]; then
|
||
|
|
fail "handoff_context='$handoff_context' does not exist on disk"
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ -n "$handoff_hash" ] && [ "$handoff_hash" != "null" ]; then
|
||
|
|
if [[ ! "$handoff_hash" =~ ^[a-f0-9]{64}$ ]]; then
|
||
|
|
fail "handoff_hash='$handoff_hash' is not a sha256 hex digest"
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
# --- Unknown keys check ---
|
||
|
|
KNOWN_KEYS="workflow phase context_compression design_doc plan build_mode build_pause subagent_dispatch tdd_mode review_mode isolation verify_mode auto_transition verify_result verification_report branch_status verified_at created_at archived direct_override build_command verify_command handoff_context handoff_hash base_ref"
|
||
|
|
while IFS=: read -r key _; do
|
||
|
|
key="${key// /}"
|
||
|
|
[ -z "$key" ] && continue
|
||
|
|
found=0
|
||
|
|
for known in $KNOWN_KEYS; do
|
||
|
|
[ "$key" = "$known" ] && found=1 && break
|
||
|
|
done
|
||
|
|
if [ "$found" -eq 0 ]; then
|
||
|
|
warn_msg "unknown field '$key' found"
|
||
|
|
fi
|
||
|
|
done < "$YAML"
|
||
|
|
|
||
|
|
# --- Summary ---
|
||
|
|
echo "" >&2
|
||
|
|
if [ "$ERRORS" -gt 0 ]; then
|
||
|
|
red "$ERRORS error(s), $WARNINGS warning(s) — validation FAILED"
|
||
|
|
exit 1
|
||
|
|
else
|
||
|
|
green "0 errors, $WARNINGS warning(s) — validation PASSED"
|
||
|
|
exit 0
|
||
|
|
fi
|