mirror of
https://github.com/rizsotto/Bear.git
synced 2026-10-30 12:15:18 +01:00
The crate move left the guides describing a layout that no longer exists: bear as the owner of semantic analysis, a build script it no longer has, and compiler YAML under crates/bear. Routing that sends a reader to the wrong directory is worse than no routing, since it reads as current. The new crates/semantic/CLAUDE.md states the constraints that are easy to violate rather than listing files: the dependency boundary that is the reason the crate exists, the deliberate public API, why the testing feature is not cfg(test), and that compiler definitions are data. docs/rationale/workspace-crate-layout.md records the durable reasoning for the split and the config boundary, including the rejected alternatives, the accepted costs, and the deferred question of folding the application library into the driver. Two sibling rationale entries described the serde deserializer this work removed; both now describe startup resolution instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
358 lines
17 KiB
Bash
Executable File
358 lines
17 KiB
Bash
Executable File
#!/bin/sh
|
|
# Build the documentation site under `site/` and verify its internal
|
|
# links. This is the build-and-link check that `.github/workflows/pages.yml`
|
|
# runs; run it before committing any change under `site/`.
|
|
#
|
|
# Run from anywhere:
|
|
# ./scripts/check-docs-site.sh
|
|
#
|
|
# The check has eight parts:
|
|
# 1. `mdbook build` must succeed AND print no WARN or ERROR line.
|
|
# Warnings are fatal on purpose: mdBook reports preprocessor and
|
|
# include failures that way, and they would otherwise ship silently.
|
|
# A chapter listed in SUMMARY.md whose file is missing is a hard
|
|
# error because `book.toml` sets `build.create-missing = false`.
|
|
# Only mdBook's log-level field is matched, not the words "warn" or
|
|
# "error" anywhere in the output: the log echoes the absolute output
|
|
# path, and a checkout under, say, /home/warner would otherwise
|
|
# always fail.
|
|
# 2. Every inline Markdown link to a `.md` target must resolve to an
|
|
# existing file. mdBook does NOT check this itself (verified against
|
|
# mdBook 0.5.4: a link to a nonexistent page builds clean), and the
|
|
# site uses no plugins, `mdbook-linkcheck` included. Scope: the scan
|
|
# is textual, so it also reads fenced code blocks and HTML comments.
|
|
# An example link to a deliberately nonexistent file inside a code
|
|
# block would be reported as broken; there is no such instance
|
|
# today. Reference-style link definitions are not scanned: by
|
|
# convention they carry external URLs only.
|
|
# 3. Every page under `site/src/` must be listed in SUMMARY.md. mdBook
|
|
# ignores files that are not, so such a page would never be served.
|
|
# A commented-out SUMMARY entry does not count as listed, and a
|
|
# `./page.md` entry counts the same as `page.md`. 404.md is exempt:
|
|
# mdBook renders it as the not-found page, and listing it would put
|
|
# it in the navigation.
|
|
# 4. Every absolute `/Bear/<page>.html` link in 404.md must have a
|
|
# matching `<page>.md`. That page links absolutely because Pages
|
|
# serves it for any missing path, so part 2 cannot follow its links.
|
|
# 5. `src/reference/supported-compilers.md` is generated, in part, from
|
|
# `crates/semantic/compilers/*.yaml` by
|
|
# `scripts/generate-supported-compilers.py`. Regenerate it into a
|
|
# scratch file and diff against the committed page: any difference
|
|
# means the YAML changed, or someone hand-edited the generated
|
|
# block, without re-running the generator. Uses only the local
|
|
# checkout (no network access) and never writes to the committed
|
|
# page.
|
|
# 6. `site/src/reference/configuration.md` must not name a config key
|
|
# that `man/bear.1.md` does not also mention. `man/bear.1.md` is
|
|
# the owner of configuration keys and defaults (see
|
|
# `site/CLAUDE.md`), so configuration.md is reference content that
|
|
# overlaps it by design; this is the drift check for that overlap,
|
|
# in place of a generator because there is no schema export to
|
|
# generate the page from. Extraction rule: every backticked,
|
|
# dot-separated, lowercase/underscore identifier of two or more
|
|
# segments in configuration.md (for example `format.paths.directory`)
|
|
# is a candidate key. For each one, its last segment (`directory`)
|
|
# must appear as a whole word somewhere in man/bear.1.md. This
|
|
# covers every key documented so far: man/bear.1.md always names a
|
|
# key's last segment, even where it does not spell out the full
|
|
# dotted path (compilers, sources, duplicates, and headers keys are
|
|
# written there under their own section heading, undotted). It does
|
|
# NOT cover a single-segment top-level key (`schema` is not
|
|
# dotted, so it is not extracted), and it does not verify a key's
|
|
# accepted values or default, only that the key name itself is not
|
|
# invented. A whole-word match is deliberately loose: it does not
|
|
# confirm the man page documents the key as configuration (a
|
|
# coincidental prose word would also pass), so this check only
|
|
# catches a key that man/bear.1.md never mentions at all, such as a
|
|
# typo or an invented key. That one-directional, name-only check is
|
|
# enough to catch the drift that matters here: a key added to the
|
|
# site page without ever being added to the man page.
|
|
# 8. Every `page.md#anchor` link must resolve to a heading that exists
|
|
# in the rendered HTML. mdBook does not check anchors and part 2
|
|
# strips them, so a renamed heading otherwise ships a link that
|
|
# lands on the right page at the wrong place.
|
|
# 7. `src/reference/command-line.md` is generated, in part, from the
|
|
# installed `bear` binary's own `--help` output by
|
|
# `scripts/generate-command-line-reference.py`. Same shape as part
|
|
# 5: regenerate into a scratch file and diff against the committed
|
|
# page. CRITICAL DIFFERENCE from part 5: that generator only reads
|
|
# the checkout, but this one needs a BUILT AND INSTALLED `bear`
|
|
# binary (see site/CLAUDE.md, "Authoritative sources"), which CI
|
|
# may not have staged at the point this script runs, and a
|
|
# contributor may not have built at all. So this part does not
|
|
# treat a missing binary as failure: the generator itself exits 2
|
|
# when it finds no usable binary (checking $BEAR_BIN, then the
|
|
# throwaway-install default), and this script treats exactly that
|
|
# exit code as "SKIPPED", printing a notice, rather than failing the
|
|
# whole check. Any other non-zero exit (malformed --help output, a
|
|
# missing template, ...) is a real problem and still fails part 7.
|
|
# This makes the script's own exit status depend on whether a
|
|
# binary happens to be present, which is unusual for this file, but
|
|
# the alternative (always failing without a binary) would make
|
|
# ./scripts/check-docs-site.sh unusable for anyone who has not
|
|
# just run a full build, including most CI stages other than the
|
|
# one that builds bear itself.
|
|
#
|
|
# mdBook must be on PATH. Install the version pinned in
|
|
# `.github/workflows/pages.yml`, which is the single source of truth for
|
|
# it.
|
|
#
|
|
# Exit codes:
|
|
# 0 - the site builds clean, every link and page checks out, the
|
|
# generated compiler and command-line pages are up to date (or
|
|
# part 7 was skipped for lack of a binary), and every
|
|
# configuration key on configuration.md is also named in the man
|
|
# page
|
|
# 1 - at least one build warning, broken link, unlisted page, a
|
|
# stale generated page, or a configuration key missing from the
|
|
# man page
|
|
# 2 - invocation error (mdbook or python3 missing, site directory not
|
|
# found)
|
|
|
|
set -eu
|
|
|
|
script_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
repo_root="$(cd "${script_dir}/.." && pwd)"
|
|
site_dir="${repo_root}/site"
|
|
src_dir="${site_dir}/src"
|
|
summary="${src_dir}/SUMMARY.md"
|
|
|
|
if [ ! -f "${site_dir}/book.toml" ]; then
|
|
echo "error: no book.toml found at ${site_dir}" >&2
|
|
exit 2
|
|
fi
|
|
|
|
if ! command -v mdbook >/dev/null 2>&1; then
|
|
echo "error: mdbook not found on PATH" >&2
|
|
echo "hint: install the version pinned in .github/workflows/pages.yml" >&2
|
|
exit 2
|
|
fi
|
|
|
|
if ! command -v python3 >/dev/null 2>&1; then
|
|
echo "error: python3 not found on PATH" >&2
|
|
exit 2
|
|
fi
|
|
|
|
log="$(mktemp)"
|
|
problems="$(mktemp)"
|
|
toc="$(mktemp)"
|
|
scratch="$(mktemp)"
|
|
keys="$(mktemp)"
|
|
trap 'rm -f "${log}" "${problems}" "${toc}" "${scratch}" "${keys}"' EXIT
|
|
|
|
# 1. Build, then treat any WARN or ERROR log line as a failure. mdBook
|
|
# 0.5 prints the level as the first field of the line.
|
|
if ! mdbook build "${site_dir}" >"${log}" 2>&1; then
|
|
cat "${log}"
|
|
echo "FAILED: mdbook build returned a non-zero status" >&2
|
|
exit 1
|
|
fi
|
|
cat "${log}"
|
|
if grep -E '^[[:space:]]*(WARN|ERROR)[[:space:]]' "${log}" >/dev/null 2>&1; then
|
|
echo "FAILED: mdbook printed WARN or ERROR lines (above)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# 2. Inline links to Markdown files must resolve. An optional link title
|
|
# (`[text](page.md "title")`) and an optional `#anchor` are stripped
|
|
# before the file is looked up.
|
|
find "${src_dir}" -name '*.md' | while read -r page; do
|
|
page_dir="$(dirname "${page}")"
|
|
grep -oE '\]\([^)]*\.md[^)]*\)' "${page}" 2>/dev/null |
|
|
sed -E 's/^\]\(//; s/\)$//; s/[[:space:]].*$//; s/#.*$//' |
|
|
grep -vE '^[a-zA-Z][a-zA-Z0-9+.-]*:' |
|
|
while read -r target; do
|
|
if [ ! -e "${page_dir}/${target}" ]; then
|
|
echo "BROKEN LINK: ${page#"${repo_root}"/} -> ${target}" \
|
|
>>"${problems}"
|
|
continue
|
|
fi
|
|
# A link that resolves OUTSIDE src/ exists on disk but not in
|
|
# the built book: mdBook rewrites it to a sibling .html that
|
|
# was never rendered, and on the deployed site it escapes the
|
|
# book root entirely. Repo files must be linked by URL.
|
|
resolved="$(cd "${page_dir}" && cd "$(dirname "${target}")" \
|
|
&& pwd)/$(basename "${target}")"
|
|
case "${resolved}" in
|
|
"${src_dir}"/*) ;;
|
|
*)
|
|
echo "LINK ESCAPES src/: ${page#"${repo_root}"/} -> ${target}" \
|
|
>>"${problems}"
|
|
;;
|
|
esac
|
|
done
|
|
done
|
|
|
|
# 3. Every page must appear in the table of contents. Collect the paths
|
|
# SUMMARY.md actually links from its chapter lines: HTML comments are
|
|
# removed first (a commented-out entry is not served), single-line
|
|
# ones before the multi-line range so that a self-contained comment
|
|
# does not open a range that swallows the rest of the file. Only lines
|
|
# that are a list item or a prefix/suffix chapter are read, and a
|
|
# leading `./` is normalized away.
|
|
sed -e 's/<!--.*-->//g' -e '/<!--/,/-->/d' "${summary}" |
|
|
grep -E '^[[:space:]]*([-*+][[:space:]]+)?\[' |
|
|
grep -oE '\]\([^)]*\)' |
|
|
sed -E 's/^\]\(//; s/\)$//; s/[[:space:]].*$//; s/#.*$//; s|^\./||' \
|
|
>"${toc}"
|
|
|
|
find "${src_dir}" -name '*.md' | while read -r page; do
|
|
relative="${page#"${src_dir}"/}"
|
|
# SUMMARY.md is the table of contents itself. 404.md is rendered by
|
|
# mdBook as the not-found page and must NOT be listed, or it would
|
|
# appear in the navigation as a chapter.
|
|
if [ "${relative}" = "SUMMARY.md" ] || [ "${relative}" = "404.md" ]; then
|
|
continue
|
|
fi
|
|
if ! grep -qxF "${relative}" "${toc}"; then
|
|
echo "NOT IN SUMMARY.md: ${relative}" >>"${problems}"
|
|
fi
|
|
done
|
|
|
|
# 4. The not-found page links with absolute `/Bear/...` URLs, because it
|
|
# is served for any missing path and relative links would resolve
|
|
# against that path. Part 2 cannot check those, so check them here:
|
|
# every `/Bear/<path>.html` must have a `<path>.md` under src/.
|
|
if [ -f "${src_dir}/404.md" ]; then
|
|
grep -oE '\(/Bear/[^)]*\.html\)' "${src_dir}/404.md" 2>/dev/null |
|
|
sed -E 's|^\(/Bear/||; s|\.html\)$||' |
|
|
while read -r target; do
|
|
if [ ! -e "${src_dir}/${target}.md" ]; then
|
|
echo "BROKEN 404 LINK: /Bear/${target}.html has no ${target}.md" \
|
|
>>"${problems}"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [ -s "${problems}" ]; then
|
|
cat "${problems}" >&2
|
|
echo "FAILED: the site has broken links or unlisted pages" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# 5. reference/supported-compilers.md must match the generator's output from
|
|
# the current compiler YAML right now. Render into a scratch file
|
|
# (the generator's own template read comes from the committed page,
|
|
# so this never modifies it) and diff.
|
|
if ! python3 "${script_dir}/generate-supported-compilers.py" "${scratch}" \
|
|
>"${log}" 2>&1; then
|
|
cat "${log}" >&2
|
|
echo "FAILED: generate-supported-compilers.py could not run" >&2
|
|
exit 1
|
|
fi
|
|
if ! diff -u "${src_dir}/reference/supported-compilers.md" "${scratch}" >"${log}"; then
|
|
cat "${log}" >&2
|
|
echo "FAILED: supported-compilers.md is stale" >&2
|
|
echo "fix: python3 scripts/generate-supported-compilers.py" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# 6. reference/configuration.md must not name a configuration key that
|
|
# man/bear.1.md never mentions. man/bear.1.md owns configuration keys
|
|
# and defaults (see site/CLAUDE.md), so configuration.md necessarily
|
|
# overlaps it; this is the drift check for that overlap, standing in
|
|
# for a generator since there is no schema export to generate the
|
|
# page from. Extraction: every backticked, dot-separated,
|
|
# lowercase/underscore identifier of two or more segments in
|
|
# configuration.md is a candidate key, for example
|
|
# `format.paths.directory`. Only the LAST segment (`directory`) is
|
|
# checked, as a whole word, against man/bear.1.md: that page always
|
|
# names a key's last segment, even for sections where it does not
|
|
# spell out the full dotted path (compilers, sources, duplicates, and
|
|
# headers keys are written there under their own section heading,
|
|
# undotted). See the header comment for what this rule does and does
|
|
# not cover.
|
|
config_page="${src_dir}/reference/configuration.md"
|
|
man_page="${repo_root}/man/bear.1.md"
|
|
if [ -f "${config_page}" ]; then
|
|
if [ ! -f "${man_page}" ]; then
|
|
echo "error: man page not found at ${man_page}" >&2
|
|
exit 2
|
|
fi
|
|
grep -oE '`[a-z][a-z0-9_]*(\.[a-z0-9_]+)+`' "${config_page}" |
|
|
sed -E 's/^`//; s/`$//' |
|
|
sort -u >"${keys}"
|
|
while read -r key; do
|
|
leaf="${key##*.}"
|
|
if ! grep -qw -F -- "${leaf}" "${man_page}"; then
|
|
echo "CONFIG KEY NOT IN MAN PAGE: configuration.md names" \
|
|
"'${key}' (checked as '${leaf}'), which man/bear.1.md" \
|
|
"never mentions" >>"${problems}"
|
|
fi
|
|
done <"${keys}"
|
|
fi
|
|
|
|
if [ -s "${problems}" ]; then
|
|
cat "${problems}" >&2
|
|
echo "FAILED: configuration.md names a key man/bear.1.md does not mention" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# 7. reference/command-line.md must match the generator's output from the
|
|
# currently installed bear binary. Unlike part 5, a binary is not
|
|
# guaranteed to exist here (see header comment), so a missing binary
|
|
# is not a failure: the generator signals that case with exit code 2,
|
|
# and this part turns exactly that into a skip, with a notice, rather
|
|
# than failing the whole script. Only a present binary is compared.
|
|
command_line_status="SKIPPED (no bear binary)"
|
|
set +e
|
|
python3 "${script_dir}/generate-command-line-reference.py" "${scratch}" \
|
|
>"${log}" 2>&1
|
|
gen_exit=$?
|
|
set -e
|
|
if [ "${gen_exit}" -eq 2 ]; then
|
|
echo "SKIPPED: command-line.md staleness check, no bear binary found:"
|
|
sed 's/^/ /' "${log}"
|
|
elif [ "${gen_exit}" -ne 0 ]; then
|
|
cat "${log}" >&2
|
|
echo "FAILED: generate-command-line-reference.py could not run" >&2
|
|
exit 1
|
|
else
|
|
if ! diff -u "${src_dir}/reference/command-line.md" "${scratch}" >"${log}"; then
|
|
cat "${log}" >&2
|
|
echo "FAILED: command-line.md is stale" >&2
|
|
echo "fix: python3 scripts/generate-command-line-reference.py" \
|
|
"(after building and installing bear to a throwaway prefix," \
|
|
"or pointing BEAR_BIN at one; see the script's header comment)" >&2
|
|
exit 1
|
|
fi
|
|
command_line_status="up to date"
|
|
fi
|
|
|
|
# 8. Every `page.md#anchor` link must point at a heading that exists.
|
|
# mdBook does not check anchors, and part 2 strips them before looking
|
|
# the file up, so a renamed heading leaves a link that lands on the
|
|
# right page at the wrong place. The rendered HTML is the only honest
|
|
# source of anchor ids, because mdBook slugifies headings, drops
|
|
# inline code markup, and de-duplicates repeats; part 1 has just
|
|
# produced it.
|
|
anchors="$(mktemp)"
|
|
trap 'rm -f "${log}" "${problems}" "${toc}" "${keys}" "${scratch}" "${anchors}"' EXIT
|
|
python3 - "${src_dir}" "${site_dir}/book" >"${anchors}" <<'PYEOF'
|
|
import os, re, sys, pathlib
|
|
|
|
src, book = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
|
|
ids = {}
|
|
for html in book.rglob("*.html"):
|
|
text = html.read_text(encoding="utf-8", errors="replace")
|
|
ids[str(html.relative_to(book))] = set(re.findall(r'id="([^"]+)"', text))
|
|
for page in sorted(src.rglob("*.md")):
|
|
for match in re.finditer(r"\]\(([^)\s]+\.md)#([^)\s]+)\)", page.read_text()):
|
|
target, anchor = match.group(1), match.group(2)
|
|
rendered = os.path.normpath(
|
|
os.path.join(page.parent.relative_to(src), target)
|
|
).replace(".md", ".html")
|
|
where = f"{page.relative_to(src)} -> {target}#{anchor}"
|
|
if rendered not in ids:
|
|
print(f"BROKEN ANCHOR: {where} (no such page)")
|
|
elif anchor not in ids[rendered]:
|
|
print(f"BROKEN ANCHOR: {where} (no such heading)")
|
|
PYEOF
|
|
if [ -s "${anchors}" ]; then
|
|
cat "${anchors}" >&2
|
|
echo "FAILED: a link points at a heading that does not exist" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "OK: site builds clean, links resolve, every page is in SUMMARY.md, supported-compilers.md is up to date, command-line.md is ${command_line_status}, and every configuration.md key is in the man page"
|