open-standards/scripts/check-public-names.sh
Transaction Science 32a35e9904 check-public-names: read file arguments from the caller's directory; name a file it cannot read
The script moved to the repository's root before reading its arguments, so paths
given from a subdirectory (a builder in wai/ or wai/wai-rs) did not exist and grep
exited 2 for the whole list; a list built from git diff after main moved, naming a
file absent from the work tree, failed the same way with nothing to say which. Each
relative argument is now resolved against the caller's directory, and every argument
that is not a readable regular file is named before the scan (still exit 2, fail
closed). The self-test gains cases for relative paths, many files, a missing file and
a directory among many.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 15:19:18 -04:00

139 lines
5.1 KiB
Shell
Executable file

#!/usr/bin/env bash
#
# check-public-names.sh
#
# Holds WAI's public text to its naming rule: it names no company, vendor,
# customer or commercial product. Standards, standards bodies, protocols,
# papers and open-source projects may be named, factually.
#
# The patterns are NOT in this repository. They are read from the
# environment variable WAI_NAME_DENYLIST: newline-separated POSIX extended
# regular expressions, matched case-sensitively (write `[Ff]oo` where both
# cases matter). In CI the variable comes from a forge Actions secret of the
# same name; locally, export it from a private file before committing:
#
# WAI_NAME_DENYLIST="$(cat /path/to/private/patterns)" scripts/check-public-names.sh
#
# A public list would itself publish the names it exists to keep out.
#
# Scope: the tracked files under wai/ with the extensions .md .rs .toml .py
# .sh .mjs .js .json .html .css .ttl .h .c .txt, and wai/'s .gitignore files;
# or, when files are given as arguments, those files (the self-test,
# scripts/test-public-text-checks.sh, uses this), each a path relative to the
# directory the script is run from, or absolute. Excluded:
# - the settled OER/2 documents and corpus (wai/extensions/oer-receipts.md,
# wai/wai-rs/src/oer.rs, wai/oer-conformance/), which no change may edit.
#
# Opt-out, for a functional identifier only (a model-hub id a tool defaults
# to, a repository URL, an OS API name, a security advisory id, a capability
# string): put `allow-name: <reason>` on the same line, in a comment of the
# file's own syntax (`<!-- allow-name: reason -->`, `// allow-name: reason`,
# `# allow-name: reason`). The reason is required: it must contain a letter or
# a digit. Prose around such an identifier describes the standard or the
# function, not the owner.
#
# Exit status: 0 clean; 1 a hit; 2 WAI_NAME_DENYLIST unset or empty, a pattern
# grep cannot compile, a file given that is not a readable regular file (each
# such file is named), or any other grep error. The check never reads an error
# as "clean".
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# The files given as arguments are paths from the caller's directory: remember it
# before moving to the repository's root, where the default scope is listed.
CALLER_DIR="$(pwd)"
cd "$REPO_ROOT"
if [ -z "${WAI_NAME_DENYLIST:-}" ]; then
echo "WAI_NAME_DENYLIST is not set: nothing to check against (see the header of $0)" >&2
exit 2
fi
patterns="$(mktemp)"
out="$(mktemp)"
trap 'rm -f "$patterns" "$out"' EXIT
printf '%s\n' "$WAI_NAME_DENYLIST" | grep -v '^[[:space:]]*$' > "$patterns" || true
if [ ! -s "$patterns" ]; then
echo "WAI_NAME_DENYLIST holds no pattern" >&2
exit 2
fi
# Compile every pattern once before scanning. grep exits 1 when nothing matches
# and 2 on an error; only 0 and 1 mean the patterns are usable. grep's message
# is suppressed: it could quote a private pattern.
set +e
grep -E -f "$patterns" /dev/null >/dev/null 2>&1
rc=$?
set -e
if [ $rc -gt 1 ]; then
echo "::error::WAI_NAME_DENYLIST holds a pattern grep cannot compile (grep exit $rc); nothing was checked" >&2
exit 2
fi
files=()
if [ $# -gt 0 ]; then
for f in "$@"; do
case "$f" in
/*) files+=("$f") ;;
*) files+=("$CALLER_DIR/$f") ;;
esac
done
else
while IFS= read -r f; do
case "$f" in
wai/extensions/oer-receipts.md|wai/wai-rs/src/oer.rs|wai/oer-conformance/*) continue ;;
esac
files+=("$f")
done < <(git ls-files 'wai/*.md' 'wai/*.rs' 'wai/*.toml' 'wai/*.py' 'wai/*.sh' 'wai/*.mjs' 'wai/*.js' \
'wai/*.json' 'wai/*.html' 'wai/*.css' 'wai/*.ttl' 'wai/*.h' 'wai/*.c' 'wai/*.txt' \
'wai/*.gitignore' 'wai/*.wgsl')
fi
if [ ${#files[@]} -eq 0 ]; then
echo "::error::no file to check" >&2
exit 2
fi
# A file grep cannot read makes it exit 2 with the whole list unchecked; name each
# such file instead of reporting a bare grep failure. File names are not private.
unreadable=0
for f in "${files[@]}"; do
if [ ! -f "$f" ] || [ ! -r "$f" ]; then
echo "::error::not a readable file: $f" >&2
unreadable=$((unreadable + 1))
fi
done
if [ $unreadable -gt 0 ]; then
echo "::error::$unreadable of ${#files[@]} files given cannot be read; nothing was checked" >&2
exit 2
fi
set +e
grep -HnE -f "$patterns" -- "${files[@]}" > "$out" 2>/dev/null
rc=$?
set -e
if [ $rc -gt 1 ]; then
echo "::error::grep failed (exit $rc) while scanning; nothing was checked" >&2
exit 2
fi
hits=0
while IFS= read -r line; do
content="${line#*:}"
content="${content#*:}"
if printf '%s' "$content" | grep -qE 'allow-name:[[:space:]]*[[:alnum:]]'; then
continue
fi
# Print the location only: the matched text may be a name the list keeps private.
if [ $hits -eq 0 ]; then
echo "::error::public text names a company, vendor, customer or product (patterns: WAI_NAME_DENYLIST)"
fi
echo "${line%%:*}:$(printf '%s' "${line#*:}" | cut -d: -f1)"
hits=$((hits + 1))
done < "$out"
if [ $hits -gt 0 ]; then
echo "Fix: name the standard, the standards body, the open-source project or the function instead;"
echo "for a functional identifier, mark the line with allow-name: <reason>."
exit 1
fi
echo "public-name check: clean (${#files[@]} files)"