mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
9b08fb933d
* docs: add go cli quality discovery plan * fix(cli): harden gc bridge version parsing * docs(go): align complexity policy with gates * docs(cli): classify command surface * fix(cli): enforce json output contracts * feat(cli): expand completion coverage * refactor(cli): pilot badge options writer * refactor(cli): inject contradict output writer * refactor(cli): inject notebook update writer * fix(cli): enable gc runtime smoke coverage * fix(cli): align gc bridge with gascity v1 * Release v2.39.0 * fix(tests): align next-work schema test with widened type enum Commited35c47fadded `docs` and `chore` to the script's VALID_TYPES, but tests/scripts/check-next-work-schema-rows.bats:42 still used `type:"docs"` as its supposed-to-be-rejected example, so the test now passes through the validator — `bats-tests` has been red on main since. Switch the rejected-type example to `finding`, which is still outside the enum and is also the canonical example cited in the script's own header comment ("type=finding ... caught at push time"). Local: 11/11 in tests/scripts/check-next-work-schema-rows.bats pass. * docs: capture finding generator postmortem (#157) * fix(rpi): sort verdicts deterministically in context and status helpers (#158) BuildPhaseContext and JoinVerdicts iterated the verdicts map directly, producing non-deterministic output across runs. Sort keys before rendering so prompt injection and status logs are reproducible. Closes council finding W-7 (context-orchestration-leverage batch). The related buildHandoffContext path was already fixed to delegate to FormatVerdicts (which sorts); this completes the remaining helpers. - cli/internal/rpi/status.go: sort keys in JoinVerdicts - cli/internal/rpi/phased_context.go: sort keys in BuildPhaseContext - tests: replace "ordering not deterministic" waivers with exact-order asserts and add a 50-run stability check https://claude.ai/code/session_01Qw4bZvXUNuLs8Rp1j1homB Co-authored-by: Claude <noreply@anthropic.com> * fix(codex): audit noisy hook injections (#159) * docs: add competitive radar (#160) * feat(agents): harden operator control plane (#161) * docs(discovery): plan agents control plane hardening * feat(agents): harden operator control plane * Release v2.39.0 * fix(tests): align next-work schema test with widened type enum Commited35c47fadded `docs` and `chore` to the script's VALID_TYPES, but tests/scripts/check-next-work-schema-rows.bats:42 still used `type:"docs"` as its supposed-to-be-rejected example, so the test now passes through the validator — `bats-tests` has been red on main since. Switch the rejected-type example to `finding`, which is still outside the enum and is also the canonical example cited in the script's own header comment ("type=finding ... caught at push time"). Local: 11/11 in tests/scripts/check-next-work-schema-rows.bats pass. * docs: capture finding generator postmortem (#157) * fix(rpi): sort verdicts deterministically in context and status helpers (#158) BuildPhaseContext and JoinVerdicts iterated the verdicts map directly, producing non-deterministic output across runs. Sort keys before rendering so prompt injection and status logs are reproducible. Closes council finding W-7 (context-orchestration-leverage batch). The related buildHandoffContext path was already fixed to delegate to FormatVerdicts (which sorts); this completes the remaining helpers. - cli/internal/rpi/status.go: sort keys in JoinVerdicts - cli/internal/rpi/phased_context.go: sort keys in BuildPhaseContext - tests: replace "ordering not deterministic" waivers with exact-order asserts and add a 50-run stability check https://claude.ai/code/session_01Qw4bZvXUNuLs8Rp1j1homB Co-authored-by: Claude <noreply@anthropic.com> * fix(codex): audit noisy hook injections (#159) * docs: add competitive radar (#160) * fix(merge): drop unused AgentsDoctorError ref and prune allowlist entries without production refs --------- Co-authored-by: Claude <noreply@anthropic.com> * fix(merge): resolve conflicts by taking PR's refactored contradict/codex_runtime, main's overnight test schema --------- Co-authored-by: Claude <noreply@anthropic.com>
381 lines
14 KiB
Bash
Executable File
381 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# check-cmdao-surface-parity.sh
|
|
#
|
|
# Command-surface parity gate for the ao CLI.
|
|
#
|
|
# Extracts leaf commands from cli/docs/COMMANDS.md and checks whether each is
|
|
# referenced in scripts/release-smoke-test.sh or cli/cmd/ao/*_test.go files.
|
|
# Commands intentionally excluded from coverage are listed in the allowlist.
|
|
#
|
|
# Usage:
|
|
# bash scripts/check-cmdao-surface-parity.sh
|
|
# bash scripts/check-cmdao-surface-parity.sh --write-surface
|
|
#
|
|
# Exit codes:
|
|
# 0 = all leaf commands are covered or allowlisted
|
|
# 1 = one or more leaf commands are uncovered and not allowlisted
|
|
#
|
|
# Environment overrides (for testing):
|
|
# CMDAO_COMMANDS_MD Path to CLI reference (default: cli/docs/COMMANDS.md)
|
|
# CMDAO_SMOKE_TEST Path to smoke test (default: scripts/release-smoke-test.sh)
|
|
# CMDAO_TEST_GLOB_DIR Directory for unit test files (default: cli/cmd/ao)
|
|
# CMDAO_ALLOWLIST Path to allowlist (default: scripts/cmdao-surface-allowlist.txt)
|
|
# CMDAO_SURFACE_MD Human command-surface inventory (default: docs/cli-surface.md)
|
|
# CMDAO_SURFACE_JSON Machine command-surface inventory (default: docs/cli-surface.json)
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
WRITE_SURFACE=false
|
|
if [[ "${1:-}" == "--write-surface" ]]; then
|
|
WRITE_SURFACE=true
|
|
elif [[ $# -gt 0 ]]; then
|
|
echo "Unknown argument: $1" >&2
|
|
echo "Usage: bash scripts/check-cmdao-surface-parity.sh [--write-surface]" >&2
|
|
exit 2
|
|
fi
|
|
|
|
COMMANDS_MD="${CMDAO_COMMANDS_MD:-$REPO_ROOT/cli/docs/COMMANDS.md}"
|
|
SMOKE_TEST="${CMDAO_SMOKE_TEST:-$REPO_ROOT/scripts/release-smoke-test.sh}"
|
|
TEST_GLOB_DIR="${CMDAO_TEST_GLOB_DIR:-$REPO_ROOT/cli/cmd/ao}"
|
|
ALLOWLIST="${CMDAO_ALLOWLIST:-$REPO_ROOT/scripts/cmdao-surface-allowlist.txt}"
|
|
SURFACE_MD="${CMDAO_SURFACE_MD:-$REPO_ROOT/docs/cli-surface.md}"
|
|
SURFACE_JSON="${CMDAO_SURFACE_JSON:-$REPO_ROOT/docs/cli-surface.json}"
|
|
|
|
# ─── Validate inputs ───────────────────────────────────────────────────────────
|
|
|
|
if [[ ! -f "$COMMANDS_MD" ]]; then
|
|
echo "CMDAO_SURFACE_PARITY: COMMANDS_MD not found: $COMMANDS_MD"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -f "$SMOKE_TEST" ]]; then
|
|
echo "CMDAO_SURFACE_PARITY: smoke test not found: $SMOKE_TEST"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -d "$TEST_GLOB_DIR" ]]; then
|
|
echo "CMDAO_SURFACE_PARITY: test directory not found: $TEST_GLOB_DIR"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -f "$ALLOWLIST" ]]; then
|
|
echo "CMDAO_SURFACE_PARITY: allowlist not found: $ALLOWLIST"
|
|
exit 1
|
|
fi
|
|
|
|
# ─── Temp workspace ────────────────────────────────────────────────────────────
|
|
|
|
TMP_DIR="$(mktemp -d)"
|
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
|
|
|
ALL_COMMANDS_FILE="$TMP_DIR/all_commands.txt"
|
|
LEAF_COMMANDS_FILE="$TMP_DIR/leaf_commands.txt"
|
|
PARENT_COMMANDS_FILE="$TMP_DIR/parent_commands.txt"
|
|
ALLOWLIST_FILE="$TMP_DIR/allowlist.txt"
|
|
ALLOWLIST_META_FILE="$TMP_DIR/allowlist_meta.tsv"
|
|
SURFACE_TSV="$TMP_DIR/cli_surface.tsv"
|
|
GENERATED_SURFACE_MD="$TMP_DIR/cli-surface.md"
|
|
GENERATED_SURFACE_JSON="$TMP_DIR/cli-surface.json"
|
|
|
|
# ─── Step 1: Extract all commands from COMMANDS.md ────────────────────────────
|
|
#
|
|
# COMMANDS.md uses heading levels to indicate command hierarchy:
|
|
# ### `ao <cmd>` — top-level command
|
|
# #### `ao <cmd> <sub>` — subcommand (one level deep)
|
|
# ##### `ao <cmd> <sub> <leaf>` — nested subcommand (two levels deep)
|
|
#
|
|
# We extract the full command string (without backticks and "ao" prefix) from
|
|
# every heading line that starts with ### / #### / #####.
|
|
|
|
extract_commands() {
|
|
local file="$1"
|
|
# Match heading lines with backtick-quoted `ao ...` commands, strip punctuation
|
|
grep -E '^#{3,5} `ao [^`]+`' "$file" \
|
|
| sed -E 's/^#{3,5} `ao ([^`]+)`.*$/\1/' \
|
|
| sed -E 's/[[:space:]]+$//' \
|
|
| grep -v '^$' \
|
|
| sort -u
|
|
}
|
|
|
|
extract_commands "$COMMANDS_MD" > "$ALL_COMMANDS_FILE"
|
|
|
|
if [[ ! -s "$ALL_COMMANDS_FILE" ]]; then
|
|
echo "CMDAO_SURFACE_PARITY: no commands parsed from $COMMANDS_MD"
|
|
echo "Expected heading style: ### \`ao <cmd>\` or #### \`ao <cmd> <sub>\`"
|
|
exit 1
|
|
fi
|
|
|
|
# ─── Step 2: Identify leaf vs. group-parent commands ─────────────────────────
|
|
#
|
|
# A command is a PARENT (group, not a leaf) if any other command starts with
|
|
# it followed by a space. All others are leaves.
|
|
|
|
while IFS= read -r cmd; do
|
|
is_parent=false
|
|
while IFS= read -r other; do
|
|
if [[ "$other" == "$cmd "* ]]; then
|
|
is_parent=true
|
|
break
|
|
fi
|
|
done < "$ALL_COMMANDS_FILE"
|
|
if $is_parent; then
|
|
echo "$cmd"
|
|
fi
|
|
done < "$ALL_COMMANDS_FILE" | sort -u > "$PARENT_COMMANDS_FILE"
|
|
|
|
comm -23 \
|
|
<(sort "$ALL_COMMANDS_FILE") \
|
|
<(sort "$PARENT_COMMANDS_FILE") \
|
|
> "$LEAF_COMMANDS_FILE"
|
|
|
|
LEAF_COUNT=$(wc -l < "$LEAF_COMMANDS_FILE" | tr -d ' ')
|
|
|
|
if [[ "$LEAF_COUNT" -eq 0 ]]; then
|
|
echo "CMDAO_SURFACE_PARITY: no leaf commands found — check COMMANDS.md structure"
|
|
exit 1
|
|
fi
|
|
|
|
# ─── Step 3: Load the allowlist ───────────────────────────────────────────────
|
|
#
|
|
# Strip comment lines (# ...) and blank lines.
|
|
# Format: category|command|reason
|
|
|
|
VALID_CATEGORIES_RE='^(public-tested|public-stateful-fixture-needed|internal-hidden|deprecated|unsafe-live|manual-only)$'
|
|
|
|
awk -F'|' -v valid_re="$VALID_CATEGORIES_RE" '
|
|
/^[[:space:]]*(#|$)/ { next }
|
|
{
|
|
if (NF != 3) {
|
|
printf "CMDAO_SURFACE_PARITY: invalid allowlist row %d: expected category|command|reason\n", NR > "/dev/stderr"
|
|
exit 1
|
|
}
|
|
category=$1
|
|
command=$2
|
|
reason=$3
|
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", category)
|
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", command)
|
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", reason)
|
|
if (category !~ valid_re) {
|
|
printf "CMDAO_SURFACE_PARITY: invalid category on row %d: %s\n", NR, category > "/dev/stderr"
|
|
exit 1
|
|
}
|
|
if (command == "" || reason == "") {
|
|
printf "CMDAO_SURFACE_PARITY: allowlist row %d needs non-empty command and reason\n", NR > "/dev/stderr"
|
|
exit 1
|
|
}
|
|
printf "%s\t%s\t%s\n", command, category, reason
|
|
}
|
|
' "$ALLOWLIST" | sort -u > "$ALLOWLIST_META_FILE"
|
|
|
|
cut -f1 "$ALLOWLIST_META_FILE" > "$ALLOWLIST_FILE" || true
|
|
|
|
printf 'command\tkind\tcategory\tcoverage_status\treason\n' > "$SURFACE_TSV"
|
|
|
|
# ─── Step 4: Check each leaf command for coverage ─────────────────────────────
|
|
#
|
|
# A leaf command is "covered" if:
|
|
# (a) Its canonical form appears in scripts/release-smoke-test.sh, OR
|
|
# (b) Its canonical form appears in any cli/cmd/ao/*_test.go file.
|
|
#
|
|
# The canonical form is "ao <subcommand>" (e.g., "ao rpi status").
|
|
# We search for the quoted form `ao <cmd>` and the unquoted form as well.
|
|
|
|
errors=0
|
|
missing=()
|
|
|
|
while IFS= read -r cmd; do
|
|
# Check allowlist first
|
|
if grep -qxF "$cmd" "$ALLOWLIST_FILE" 2>/dev/null; then
|
|
awk -F'\t' -v cmd="$cmd" '$1 == cmd { printf "%s\tleaf\t%s\tallowlisted\t%s\n", $1, $2, $3 }' "$ALLOWLIST_META_FILE" >> "$SURFACE_TSV"
|
|
continue
|
|
fi
|
|
|
|
# Build search patterns: the leaf subcommand portion (last word(s) after first token)
|
|
# e.g., "rpi status" → search for "ao rpi status", "rpi status", '"rpi status"'
|
|
full_cmd="ao $cmd"
|
|
|
|
covered=false
|
|
|
|
# Search smoke test
|
|
if grep -qF "$full_cmd" "$SMOKE_TEST" 2>/dev/null; then
|
|
covered=true
|
|
fi
|
|
|
|
# Search *_test.go files (only if not already found)
|
|
if ! $covered; then
|
|
if grep -rlF "$full_cmd" "$TEST_GLOB_DIR/"*_test.go >/dev/null 2>/dev/null; then
|
|
covered=true
|
|
fi
|
|
fi
|
|
|
|
# Also search for the subcommand path without "ao " prefix (e.g., "rpi status")
|
|
# Some tests reference subcommand names without the binary prefix
|
|
if ! $covered; then
|
|
# Only search test files for bare subcommand if it's a multi-word command
|
|
# (single-word commands have too many false positives)
|
|
if [[ "$cmd" == *" "* ]]; then
|
|
if grep -rlF "$cmd" "$TEST_GLOB_DIR/"*_test.go >/dev/null 2>/dev/null; then
|
|
covered=true
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Dedicated test file heuristic: a file named after the command (with spaces
|
|
# and hyphens normalized to underscores) implies coverage even if the
|
|
# "ao <cmd>" string literal never appears. Examples:
|
|
# retrieval-bench → retrieval_bench_test.go / retrieval_bench_*_test.go
|
|
# handoff → handoff_test.go
|
|
# rpi serve → rpi_serve_test.go
|
|
if ! $covered; then
|
|
basename_pat=$(printf '%s' "$cmd" | tr ' -' '__')
|
|
# Use `find` so missing patterns don't fail under `set -euo pipefail`.
|
|
if [[ -f "$TEST_GLOB_DIR/${basename_pat}_test.go" ]] \
|
|
|| find "$TEST_GLOB_DIR" -maxdepth 1 -name "${basename_pat}_*_test.go" -print -quit 2>/dev/null | grep -q .; then
|
|
covered=true
|
|
fi
|
|
fi
|
|
|
|
# executeCommand / Cobra SetArgs heuristic: Go tests often invoke subcommands
|
|
# as comma-separated string args (e.g., executeCommand("scenario", "list"))
|
|
# rather than as the literal "ao scenario list" token the earlier checks look
|
|
# for. Detect this pattern for multi-word commands.
|
|
if ! $covered && [[ "$cmd" == *" "* ]]; then
|
|
# Build a regex like: "scenario"[[:space:]]*,[[:space:]]*"list"
|
|
quoted_tokens=$(printf '%s' "$cmd" | awk '{for(i=1;i<=NF;i++){printf "\"%s\"", $i; if(i<NF) printf "[[:space:]]*,[[:space:]]*"}}')
|
|
if grep -rlE "$quoted_tokens" "$TEST_GLOB_DIR"/*_test.go >/dev/null 2>/dev/null; then
|
|
covered=true
|
|
fi
|
|
|
|
# Command catalog map heuristic: test files enumerate command trees as
|
|
# `"<parent>": {"<leaf>", "<leaf>"}`. This pattern is used by command-
|
|
# registration smoke tests in cobra_commands_test.go and similar.
|
|
if ! $covered; then
|
|
parent=$(printf '%s' "$cmd" | awk '{print $1}')
|
|
leaf=$(printf '%s' "$cmd" | awk '{for(i=2;i<=NF;i++){printf "%s%s", $i, (i<NF?" ":"")}}')
|
|
map_pattern="\"$parent\"[[:space:]]*:[[:space:]]*\\{[^}]*\"$leaf\""
|
|
if grep -rlE "$map_pattern" "$TEST_GLOB_DIR"/*_test.go >/dev/null 2>/dev/null; then
|
|
covered=true
|
|
fi
|
|
fi
|
|
|
|
# Handler-function naming convention: `runXxxYyy` mirrors `ao xxx yyy`. The
|
|
# Go codebase wires Cobra subcommands to handlers with this pattern, and
|
|
# unit tests often call the handler directly rather than routing through
|
|
# cobra.
|
|
if ! $covered; then
|
|
title_cased=$(printf '%s' "$cmd" | awk '{for(i=1;i<=NF;i++){s=$i; printf "%s%s", toupper(substr(s,1,1)), substr(s,2)}}')
|
|
# Also fold hyphens: e.g., "config models" is fine but "rpi verify-final" would need hyphen→case.
|
|
title_cased=$(printf '%s' "$title_cased" | awk 'BEGIN{FS="-"; OFS=""} {for(i=1;i<=NF;i++){s=$i; printf "%s%s", toupper(substr(s,1,1)), substr(s,2)}}')
|
|
if grep -rlE "\\brun${title_cased}\\b" "$TEST_GLOB_DIR"/*_test.go >/dev/null 2>/dev/null; then
|
|
covered=true
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if ! $covered; then
|
|
missing+=("$cmd")
|
|
errors=$((errors + 1))
|
|
printf '%s\tleaf\tmanual-only\tmissing\tNo smoke, direct test, or allowlist coverage found.\n' "$cmd" >> "$SURFACE_TSV"
|
|
else
|
|
printf '%s\tleaf\tpublic-tested\tcovered\tCovered by release smoke tests, direct command tests, or command handler tests.\n' "$cmd" >> "$SURFACE_TSV"
|
|
fi
|
|
|
|
done < "$LEAF_COMMANDS_FILE"
|
|
|
|
# ─── Step 5: Generate or check command-surface sidecars ───────────────────────
|
|
|
|
python3 - "$SURFACE_TSV" "$GENERATED_SURFACE_MD" "$GENERATED_SURFACE_JSON" <<'PY'
|
|
import csv
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
tsv_path, md_path, json_path = map(Path, sys.argv[1:4])
|
|
with tsv_path.open(newline="") as f:
|
|
rows = list(csv.DictReader(f, delimiter="\t"))
|
|
|
|
payload = {
|
|
"schema_version": 1,
|
|
"generated_by": "scripts/check-cmdao-surface-parity.sh --write-surface",
|
|
"categories": [
|
|
"public-tested",
|
|
"public-stateful-fixture-needed",
|
|
"internal-hidden",
|
|
"deprecated",
|
|
"unsafe-live",
|
|
"manual-only",
|
|
],
|
|
"commands": rows,
|
|
}
|
|
|
|
json_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
|
|
|
lines = [
|
|
"# ao CLI Command Surface",
|
|
"",
|
|
"> Generated by `scripts/check-cmdao-surface-parity.sh --write-surface`.",
|
|
"> Do not edit command rows manually.",
|
|
"",
|
|
"| Command | Category | Coverage | Reason |",
|
|
"|---------|----------|----------|--------|",
|
|
]
|
|
for row in rows:
|
|
command = row["command"].replace("|", "\\|")
|
|
category = row["category"].replace("|", "\\|")
|
|
coverage = row["coverage_status"].replace("|", "\\|")
|
|
reason = row["reason"].replace("|", "\\|")
|
|
lines.append(f"| `ao {command}` | `{category}` | `{coverage}` | {reason} |")
|
|
|
|
md_path.write_text("\n".join(lines) + "\n")
|
|
PY
|
|
|
|
if $WRITE_SURFACE; then
|
|
mkdir -p "$(dirname "$SURFACE_MD")" "$(dirname "$SURFACE_JSON")"
|
|
cp "$GENERATED_SURFACE_MD" "$SURFACE_MD"
|
|
cp "$GENERATED_SURFACE_JSON" "$SURFACE_JSON"
|
|
else
|
|
if [[ ! -f "$SURFACE_MD" || ! -f "$SURFACE_JSON" ]]; then
|
|
echo "CMDAO_SURFACE_PARITY: command-surface sidecars are missing."
|
|
echo "Run: bash scripts/check-cmdao-surface-parity.sh --write-surface"
|
|
exit 1
|
|
fi
|
|
if ! diff -q "$SURFACE_MD" "$GENERATED_SURFACE_MD" >/dev/null 2>&1; then
|
|
echo "CMDAO_SURFACE_PARITY: $SURFACE_MD is out of date."
|
|
echo "Run: bash scripts/check-cmdao-surface-parity.sh --write-surface"
|
|
diff -u "$SURFACE_MD" "$GENERATED_SURFACE_MD" || true
|
|
exit 1
|
|
fi
|
|
if ! diff -q "$SURFACE_JSON" "$GENERATED_SURFACE_JSON" >/dev/null 2>&1; then
|
|
echo "CMDAO_SURFACE_PARITY: $SURFACE_JSON is out of date."
|
|
echo "Run: bash scripts/check-cmdao-surface-parity.sh --write-surface"
|
|
diff -u "$SURFACE_JSON" "$GENERATED_SURFACE_JSON" || true
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# ─── Step 6: Report ───────────────────────────────────────────────────────────
|
|
|
|
ALLOWLIST_COUNT=$(wc -l < "$ALLOWLIST_FILE" | tr -d ' ')
|
|
|
|
echo "CMDAO_SURFACE_PARITY: $LEAF_COUNT leaf commands, $ALLOWLIST_COUNT allowlisted"
|
|
|
|
if [[ "${#missing[@]}" -gt 0 ]]; then
|
|
echo ""
|
|
echo "CMDAO_SURFACE_PARITY: UNCOVERED commands (not in smoke test, not in *_test.go, not allowlisted):"
|
|
for cmd in "${missing[@]}"; do
|
|
echo " - $cmd"
|
|
done
|
|
echo ""
|
|
echo "Action: add coverage to scripts/release-smoke-test.sh or cli/cmd/ao/*_test.go,"
|
|
echo " or add a category|command|reason row to scripts/cmdao-surface-allowlist.txt."
|
|
echo ""
|
|
echo "CMDAO_SURFACE_PARITY: FAILED ($errors uncovered command(s))"
|
|
exit 1
|
|
fi
|
|
|
|
echo "CMDAO_SURFACE_PARITY: PASS (all $LEAF_COUNT leaf commands covered or allowlisted)"
|
|
exit 0
|