feat(11.1-02): check module identifiers in docs and READMEs
- go/parser index of every modules/ package and sub-package, with methods, fields, interface methods and promoted members - code spans in docs pages, module READMEs and the root README fail Check and docs:build when the named identifier does not exist - scripts/check-phase11.1.sh with preconditions, deps, docs, forbidden, go and a self-test that plants one violation per rule
This commit is contained in:
199
scripts/check-phase11.1.sh
Executable file
199
scripts/check-phase11.1.sh
Executable file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env bash
|
||||
# Phase 11.1 fail-closed gate (documentation for humans and AI agents:
|
||||
# DOCS-01 to DOCS-08).
|
||||
#
|
||||
# The semantic checks (identifiers, links and anchors, src= snippets,
|
||||
# command names, consuming-application names, fence policy) live in Go and
|
||||
# run through `go test` and `summer docs:build --check`. This script only
|
||||
# orchestrates them and adds repository-level assertions: preconditions,
|
||||
# the dependency delta, the built output and the forbidden-name sweep.
|
||||
# --self-test plants one violation per rule in a scratch copy and requires
|
||||
# docs:build --check to refuse it for that rule.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="${PHASE11_1_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||
PHASE11_DIR="$ROOT/.planning/phases/11-jobs-realtime-and-search-infrastructure"
|
||||
DEPS_BASE="${PHASE11_1_DEPS_BASE:-9033d81}"
|
||||
# Module paths this phase may add to go.mod (D-15).
|
||||
ALLOWED_NEW_DEPS="github.com/alecthomas/chroma/v2 github.com/dlclark/regexp2/v2"
|
||||
# Phase 11 modules that must be documented before the docs are written.
|
||||
PHASE11_MODULES=(conga lighthouse flare beachcomber)
|
||||
# D-11 consuming-application spellings, byte-level alternation so it works
|
||||
# in any locale (the accented variant matches the Phase 11 hygiene regex).
|
||||
FORBIDDEN_RE='fonoteka|p(l|ł|Ł)(y|ý|Ý)tarium'
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
usage:
|
||||
check-phase11.1.sh --preconditions
|
||||
check-phase11.1.sh --deps
|
||||
check-phase11.1.sh --docs
|
||||
check-phase11.1.sh --forbidden
|
||||
check-phase11.1.sh --self-test
|
||||
check-phase11.1.sh --go
|
||||
check-phase11.1.sh --all
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
refuse() {
|
||||
echo "refuse: $*" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# build_summer compiles the summer tool into $1 once per mode.
|
||||
build_summer() {
|
||||
(cd "$ROOT" && go build -o "$1" ./cmd/summer)
|
||||
}
|
||||
|
||||
run_preconditions() {
|
||||
[ -f "$PHASE11_DIR/11-08-SUMMARY.md" ] || refuse "gap plan 11-08 has no SUMMARY (module READMEs are not stable yet)"
|
||||
local m
|
||||
for m in "${PHASE11_MODULES[@]}"; do
|
||||
[ -f "$ROOT/modules/$m/README.md" ] || refuse "modules/$m has no README.md"
|
||||
grep -qF "| [$m](modules/$m/README.md) |" "$ROOT/README.md" || refuse "root README has no table row for $m"
|
||||
done
|
||||
echo "phase11.1 preconditions passed"
|
||||
}
|
||||
|
||||
# module_paths prints the module paths required by a go.mod file.
|
||||
module_paths() {
|
||||
(cd "$ROOT" && go mod edit -json "$1") | python3 -c '
|
||||
import json, sys
|
||||
for r in json.load(sys.stdin).get("Require") or []:
|
||||
print(r["Path"])
|
||||
' | sort -u
|
||||
}
|
||||
|
||||
run_deps() {
|
||||
local tmp base now removed added dep
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' RETURN
|
||||
git -C "$ROOT" show "$DEPS_BASE:go.mod" >"$tmp/base.mod"
|
||||
cp "$ROOT/go.mod" "$tmp/now.mod"
|
||||
base="$(module_paths "$tmp/base.mod")"
|
||||
now="$(module_paths "$tmp/now.mod")"
|
||||
removed="$(comm -23 <(echo "$base") <(echo "$now"))"
|
||||
[ -z "$removed" ] || refuse "go.mod dropped modules since $DEPS_BASE: $removed"
|
||||
added="$(comm -13 <(echo "$base") <(echo "$now"))"
|
||||
for dep in $added; do
|
||||
case " $ALLOWED_NEW_DEPS " in
|
||||
*" $dep "*) ;;
|
||||
*) refuse "go.mod added $dep, which no phase decision approves" ;;
|
||||
esac
|
||||
done
|
||||
echo "phase11.1 deps passed"
|
||||
}
|
||||
|
||||
run_docs() {
|
||||
local tmp site name
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' RETURN
|
||||
build_summer "$tmp/summer"
|
||||
site="$tmp/site"
|
||||
(cd "$ROOT" && "$tmp/summer" docs:build --root "$ROOT" --out "$site") || refuse "docs:build failed"
|
||||
for name in index.html index.md llms.txt llms-full.txt search-index.json assets/site.css .summer-docs; do
|
||||
[ -f "$site/$name" ] || refuse "docs:build output has no $name"
|
||||
done
|
||||
if find "$site" -name '*.go' | grep -q .; then
|
||||
refuse "docs:build output contains .go files"
|
||||
fi
|
||||
echo "phase11.1 docs passed"
|
||||
}
|
||||
|
||||
# forbidden_hits prints the files under the given paths that name a
|
||||
# consuming application, never the matching lines.
|
||||
forbidden_hits() {
|
||||
grep -rliE "$FORBIDDEN_RE" "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
run_forbidden() {
|
||||
local tmp hits
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' RETURN
|
||||
build_summer "$tmp/summer"
|
||||
(cd "$ROOT" && "$tmp/summer" docs:build --root "$ROOT" --out "$tmp/site" >/dev/null) || refuse "docs:build failed"
|
||||
hits="$(forbidden_hits "$ROOT/docs" "$tmp/site")"
|
||||
[ -z "$hits" ] || refuse "consuming-application name in: $(echo "$hits" | sed "s|$tmp/||; s|$ROOT/||" | tr '\n' ' ')"
|
||||
echo "phase11.1 forbidden passed"
|
||||
}
|
||||
|
||||
run_go() {
|
||||
(cd "$ROOT" && go vet ./...)
|
||||
(cd "$ROOT" && go test ./...)
|
||||
echo "phase11.1 go passed"
|
||||
}
|
||||
|
||||
# expect_refusal runs docs:build --check on the scratch root and requires a
|
||||
# non-zero exit whose output names the rule.
|
||||
expect_refusal() {
|
||||
local name="$1" want="$2" summer="$3" root="$4" output
|
||||
if output="$(cd "$root" && GOWORK=off "$summer" docs:build --check --root "$root" 2>&1)"; then
|
||||
echo "refuse: self-test accepted planted $name" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -Fq -- "$want" <<<"$output"; then
|
||||
echo "refuse: self-test $name failed for the wrong rule: $output" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# restore copies a pristine file back over its planted scratch copy.
|
||||
restore() {
|
||||
cp "$1/$3" "$2/$3"
|
||||
}
|
||||
|
||||
run_self_test() {
|
||||
bash -n "${BASH_SOURCE[0]}"
|
||||
local tmp scratch pristine summer
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' RETURN
|
||||
scratch="$tmp/root"
|
||||
pristine="$tmp/pristine"
|
||||
summer="$tmp/summer"
|
||||
build_summer "$summer"
|
||||
mkdir -p "$scratch" "$pristine"
|
||||
cp -R "$ROOT/docs" "$ROOT/modules" "$ROOT/go.mod" "$ROOT/go.sum" "$scratch/"
|
||||
cp -R "$ROOT/docs" "$pristine/"
|
||||
|
||||
(cd "$scratch" && GOWORK=off "$summer" docs:build --check --root "$scratch" >/dev/null) ||
|
||||
refuse "self-test baseline: the unplanted scratch copy does not pass docs:build --check"
|
||||
|
||||
printf '\nSee `bonfire.NoSuchThing`.\n' >>"$scratch/docs/index.md"
|
||||
expect_refusal 'unknown identifier' 'identifier: bonfire.NoSuchThing does not exist in modules/bonfire' "$summer" "$scratch"
|
||||
restore "$pristine" "$scratch" docs/index.md
|
||||
|
||||
mkdir "$scratch/modules/zzplant"
|
||||
printf 'package zzplant\n' >"$scratch/modules/zzplant/zzplant.go"
|
||||
expect_refusal 'missing README' 'readme: package has Go files but no README.md' "$summer" "$scratch"
|
||||
rm -rf "$scratch/modules/zzplant"
|
||||
|
||||
sed -i 's/Hello, %s/Hi, %s/' "$scratch/docs/setup/installation.md"
|
||||
expect_refusal 'drifted snippet' 'snippet: body differs' "$summer" "$scratch"
|
||||
restore "$pristine" "$scratch" docs/setup/installation.md
|
||||
|
||||
sed -i '1a colour: red' "$scratch/docs/setup/installation.md"
|
||||
expect_refusal 'frontmatter typo' 'frontmatter: unknown field' "$summer" "$scratch"
|
||||
restore "$pristine" "$scratch" docs/setup/installation.md
|
||||
|
||||
echo "phase11.1 self-test passed"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
--preconditions) run_preconditions ;;
|
||||
--deps) run_deps ;;
|
||||
--docs) run_docs ;;
|
||||
--forbidden) run_forbidden ;;
|
||||
--self-test) run_self_test ;;
|
||||
--go) run_go ;;
|
||||
--all)
|
||||
run_preconditions
|
||||
run_deps
|
||||
run_self_test
|
||||
run_docs
|
||||
run_forbidden
|
||||
run_go
|
||||
echo "phase11.1 all passed"
|
||||
;;
|
||||
*) usage ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user