Files
moonwell-client-core/scripts/genmap.sh
T
Sam Sung e3097df6a4 The deviation list writes each comment's whole paragraph (#2883)
`docs/METHOD.md` asks each deviation to name the reference fact and why benilla differs, and `docs/DEVIATIONS.md` is generated from those comments. The generator wrote only a comment's lead: its first sentence, extended to at least 70 characters. A comment that gives its reason first lost the reference fact from the list, as the Warden entry did after its reason was restored.

The list now writes each comment's whole paragraph, the one the reason check already reads. The lead and sentence helpers are gone with it.

Tested by regenerating the list: every entry keeps its full paragraph, and the Warden entry shows the reference's handling of `SMSG_WARDEN_DATA` again.

Fixes #2880

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:32:45 +07:00

442 lines
22 KiB
Bash
Executable File

#!/usr/bin/env bash
# Generate docs/MAP.md, the map of what is built, and docs/DEVIATIONS.md, where benilla differs from
# 1.12.1, both from what is on disk.
#
# Regenerated and committed at every land; run it by hand only to look, never edit the output. No
# timestamps, so an identical tree gives an identical file. No `pipefail`/`-e`: a grep that finds
# nothing (a single-file lib, a crate with no bins) must not abort the generator. Bash and POSIX
# awk only (macOS's awk is the floor): no gawk extensions, no new dependencies.
set -u
cd "$(dirname "$0")/.."
# `--check` runs every stage but writes the docs into a scratch folder, for the gates.
out=docs
if [ "${1:-}" = "--check" ]; then out=$(mktemp -d "${TMPDIR:-/tmp}/genmap-out.XXXXXX"); trap 'rm -rf "$out"' EXIT; fi
# Extract a quoted `key = "value"` field from a Cargo.toml.
field() { grep -m1 "^$2" "$1" 2>/dev/null | sed -E "s/^$2[[:space:]]*=[[:space:]]*\"//; s/\".*$//"; }
{
echo "# benilla — generated map"
echo
echo "> **GENERATED** by \`scripts/genmap.sh\` from the code — do not edit by hand; rerun the script"
echo "> when what's built changes. *What-is* only: the *what-changed* is in git. No timestamp —"
echo "> identical code yields an identical map."
echo
echo "## Crates"
echo
for ct in crates/*/Cargo.toml; do
name=$(field "$ct" name)
desc=$(field "$ct" description)
# Crates here write `bevy.workspace = true`: the trailing class accepts `.`, whitespace and `=`
# (`bevy = { … }`, `bevy="…"` too) and still rejects a `bevyfoo` crate.
if grep -qE '^(bevy|bevy_egui|avian3d)[.[:space:]=]' "$ct"; then bevy="Bevy"; else bevy="no Bevy"; fi
echo "- **$name** ($bevy) — ${desc:-—}"
done
echo
echo "## App subsystems — Bevy plugins in load order (\`crates/benilla-app/src/lib.rs\`)"
echo
# Accept bare (`add_plugins(FooPlugin)`) and path-qualified (`add_plugins(foo::FooPlugin)`)
# registrations; print the plugin type name either way.
grep -oE 'add_plugins\(([a-z_]+::)*[A-Za-z_]+Plugins?' crates/benilla-app/src/lib.rs \
| sed -E 's/add_plugins\(//; s/([a-z_]+::)+//' | awk '!seen[$0]++ {print "- " $0}'
echo
# `lib.rs` adds each dev group in one line, so their members are read out of `dev.rs`.
echo "### Behind the \`dev\` feature (\`crates/benilla-app/src/dev.rs\` — compiled out by \`--no-default-features\`)"
echo
grep -oE 'add_plugins\(([a-z_]+::)*[A-Za-z_]+Plugins?' crates/benilla-app/src/dev.rs \
| sed -E 's/add_plugins\(//; s/([a-z_]+::)+//' | awk '!seen[$0]++ {print "- " $0}'
echo
echo "## Modules (top-level, per crate)"
echo
for lib in crates/*/src/lib.rs crates/*/src/main.rs; do
[ -f "$lib" ] || continue
crate=$(echo "$lib" | sed -E 's@crates/([^/]+)/.*@\1@')
mods=$(grep -E '^[[:space:]]*(pub(\([a-z]+\))? )?mod [a-z_0-9]+;' "$lib" \
| sed -E 's/.*mod ([a-z_0-9]+);.*/\1/' | sort | tr '\n' ' ')
[ -n "$mods" ] && echo "- **$crate** ($(basename "$lib")): $mods"
done
echo
echo "## CLI binaries"
echo
# Both forms cargo accepts: `src/bin/<name>.rs` and `src/bin/<name>/main.rs`.
for b in crates/*/src/bin/*.rs crates/*/src/bin/*/main.rs; do
[ -f "$b" ] || continue
crate=$(echo "$b" | sed -E 's@crates/([^/]+)/.*@\1@')
case "$b" in
*/main.rs) name=$(basename "$(dirname "$b")") ;;
*) name=$(basename "$b" .rs) ;;
esac
echo "- \`$name\` (in $crate)"
done | LC_ALL=C sort
echo
# Shaders are embedded and addressed by their owning crate, so the crate is part of the name.
echo "## WGSL shaders (\`crates/*/src/shaders/\`, embedded)"
echo
for s in crates/*/src/shaders/*.wgsl; do
[ -f "$s" ] || continue
crate=$(echo "$s" | sed -E 's@crates/([^/]+)/.*@\1@' | tr - _)
echo "- \`embedded://$crate/shaders/$(basename "$s")\`"
done
echo
echo "## Instruments — \`\$WOW_*\` switches (env-var read sites)"
echo
echo "> The headless/dev instrument fleet, discovered from the code: every \`WOW_*\` env var"
echo "> some \`.rs\` reads, and where it's read — the doc comment at the read site is the"
# Name no dev keys here: they are listed once, in the debug panel's footer.
echo "> semantics. (The in-window surfaces — the Ctrl+Shift dev-chord overlays — are plugins"
echo "> above; this indexes the switches that don't announce themselves.)"
echo
# Match any quoted "WOW_*" literal: reads also go through helpers (`knob("WOW_FX_AGE", …)`), and
# doc comments write the unquoted form. The probe registry `capture/probe_env.rs` is a table,
# not a read site, so it is left out.
grep -rHoE '"WOW_[A-Z0-9_]+"' crates --include='*.rs' 2>/dev/null \
| grep -v '^crates/benilla-app/src/capture/probe_env\.rs:' \
| sed -E 's@^crates/@@; s/:"/\t/; s/"$//' \
| awk -F'\t' '{print $2 "\t" $1}' | sort -u \
| awk -F'\t' '$1 != v { if (v) print line; v = $1; line = "- `" $1 "` — " $2; next }
{ line = line ", " $2 }
END { if (v) print line }'
echo
# Every script, with the first sentence of its own header.
echo "## Scripts (\`scripts/\`)"
echo
for f in scripts/*.py scripts/*.sh; do
[ -f "$f" ] || continue
# The summary: the header's first non-empty content after the shebang, comment or docstring
# markers stripped, joined until its first blank line, cut at a sentence end or ~150 chars.
summary=$(LC_ALL=C awk '
NR == 1 && /^#!/ { next }
/^@echo off/ { next }
{ line = $0
sub(/^[[:space:]]*(r?"""|#|\/\/|rem)[[:space:]]?/, "", line)
if (line ~ /^[[:space:]]*$/) { if (got) exit; else next }
if (line ~ /^"""/) exit
got = 1; out = out (out ? " " : "") line }
END { print out }' "$f" | sed -E 's/\*\*//g; s/^[A-Za-z0-9_.\/-]+ (—|--) //; s/^([^.]*[.!?])([[:space:]]|$).*/\1/; s/^(.{150}).+/\1…/')
echo "- \`${f#scripts/}\` — $summary"
done
echo
} > "$out/MAP.md"
echo "genmap: wrote $out/MAP.md"
# ── docs/DEVIATIONS.md ─────────────────────────────────────────────────────────────────────────────
# Every place benilla writes down that it differs from 1.12.1: the `Deviates` rows of the CVar table,
# and each `Deviation:` comment in the crates (Rust and WGSL `//`, Lua `--`, XML `<!-- -->`). A
# deviation's wording is read from where it lives, so the list cannot drift from the code.
tmp=$(mktemp -d "${TMPDIR:-/tmp}/genmap.XXXXXX") || exit 1
trap 'rm -rf "$tmp"; [ "$out" = docs ] || rm -rf "$out"' EXIT
# Test code is left out: a file under `tests/`, named `tests.rs` or `*_tests.rs`, or declared by a
# `#[cfg(test)] mod x;` (listed here, resolved the way rustc does, `#[path]` included), and a
# `#[cfg(test)] mod x { … }` body (skipped in the extractor below, which closes it at the first `}`
# in the module's own column; rustfmt keeps it there).
grep -rlE --include='*.rs' 'cfg\(test\)' crates 2>/dev/null | LC_ALL=C sort | xargs awk '
FNR == 1 { pend = 0; path = ""; base = FILENAME; sub(/[^\/]*$/, "", base)
stem = FILENAME; sub(/.*\//, "", stem); sub(/\.rs$/, "", stem)
own = (stem == "mod" || stem == "lib" || stem == "main") ? base : base stem "/" }
{ t = $0; sub(/^[ \t]+/, "", t) }
t == "#[cfg(test)]" { pend = 1; path = ""; next }
pend && t ~ /^#\[path = "/ { path = t; sub(/^#\[path = "/, "", path); sub(/".*/, "", path); next }
pend && t ~ /^#\[/ { next }
pend {
pend = 0
if (t ~ /^(pub(\([a-z]+\))? )?mod [A-Za-z_0-9]+;/) {
name = t; sub(/^(pub(\([a-z]+\))? )?mod /, "", name); sub(/;.*/, "", name)
if (path != "") print base path
else { print own name ".rs"; print own name "/mod.rs" }
}
}' > "$tmp/testmods" 2>/dev/null
# Stage 1: one TSV row per `Deviation:` comment: crate, module, path in the crate, line, sentence.
LC_ALL=C grep -rIlE 'Deviation:' crates 2>/dev/null | LC_ALL=C sort | grep -vE '/tests/|(^|/)tests\.rs$|_tests\.rs$' \
| xargs env LC_ALL=C awk -v list="$tmp/testmods" '
function indent_of(s, t) { t = s; sub(/[^ \t].*$/, "", t); return length(t) }
function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
# Where a `Deviation:` label starts in a comment, 0 if none: one inside backticks is a mention.
function label_at(c, at, off, rest) {
off = 0; rest = c
while ((at = index(rest, "Deviation:")) > 0) {
if (substr(rest, at - 1, 1) != "`" || at == 1) return off + at
off += at + 9; rest = substr(rest, at + 10)
}
return 0
}
function module_of(r, rest, m) {
if (r ~ /^src\//) {
rest = substr(r, 5)
if (index(rest, "/")) { sub(/\/.*/, "", rest); return rest }
sub(/\.[^.]*$/, "", rest)
return (rest == "lib" || rest == "main") ? "(crate root)" : rest
}
if (r ~ /^assets\//) { rest = substr(r, 8); m = "assets"; if (index(rest, "/")) { sub(/\/.*/, "", rest); m = m "/" rest } return m }
sub(/\/.*/, "", r); return r
}
function process(f, n, i, j, s, t, ext, xin, skipping, tindent, pend, p, q, c, kind, code, marker, text, lab, cr, rel, first, cont, full) {
n = 0; delete L; delete K; delete C; delete Q; delete XI; delete T
while ((getline s < f) > 0) L[++n] = s
close(f)
ext = f; sub(/.*\./, "", ext)
if (ext != "rs" && ext != "wgsl" && ext != "xml" && ext != "lua") return
cr = f; sub(/^crates\//, "", cr); sub(/\/.*/, "", cr)
rel = f; sub(/^crates\/[^\/]*\//, "", rel)
xin = 0; skipping = 0; pend = 0
for (i = 1; i <= n; i++) {
s = L[i]; t = s; sub(/^[ \t]+/, "", t)
T[i] = 0
if (ext == "rs") {
if (skipping) { T[i] = 1; if (indent_of(s) == tindent && t ~ /^}/) skipping = 0 }
else if (t == "#[cfg(test)]") pend = 1
else if (pend) {
if (t !~ /^#\[/) {
pend = 0
if (t ~ /^(pub(\([a-z]+\))? )?mod [A-Za-z_0-9]+ *\{[ \t]*$/) { skipping = 1; tindent = indent_of(s); T[i] = 1 }
}
}
}
XI[i] = xin; K[i] = ""; C[i] = ""; Q[i] = 0
if (xin) {
K[i] = "x"; c = s
if ((q = index(c, "-->")) > 0) { c = substr(c, 1, q - 1); xin = 0 }
C[i] = trim(c)
} else if (ext == "xml" || ext == "lua") {
p = index(s, "<!--")
if (p > 0 && ext == "xml") {
K[i] = "x"; c = substr(s, p + 4); Q[i] = (trim(substr(s, 1, p - 1)) != "")
if ((q = index(c, "-->")) > 0) c = substr(c, 1, q - 1); else xin = 1
C[i] = trim(c)
} else if ((p = index(s, "--")) > 0) {
K[i] = "--"; c = substr(s, p + 2); sub(/^-+/, "", c); Q[i] = (trim(substr(s, 1, p - 1)) != "")
C[i] = trim(c)
}
} else {
p = index(s, "//")
if (p > 0) {
c = substr(s, p + 2); marker = "//"
if (substr(c, 1, 1) == "/") { c = substr(c, 2); marker = "///" }
else if (substr(c, 1, 1) == "!") { c = substr(c, 2); marker = "//!" }
K[i] = marker; Q[i] = (trim(substr(s, 1, p - 1)) != "")
C[i] = trim(c)
}
}
}
for (i = 1; i <= n; i++) {
if (K[i] == "" || (lab = label_at(C[i])) == 0) continue
text = trim(substr(C[i], lab + 10))
# Continue over the comment lines that follow, to its paragraph end: a blank comment line,
# a list item, a code fence or the next `Deviation:` ends it.
for (j = i + 1; j <= n; j++) {
if (K[i] == "x") { if (!XI[j] || K[j] != "x") break }
else if (K[j] != K[i] || Q[j]) break
cont = C[j]
if (cont == "" || cont ~ /^([-*] |```)/ || label_at(cont)) break
text = text " " cont
}
full = trim(text); gsub(/\t/, " ", full)
# The whole paragraph is written, so the list carries the reference fact with the reason.
text = trim(text)
gsub(/\t/, " ", text)
# A rustdoc link [`x`] or [`x`](path) reads as `x`.
while (match(text, /\[`[^`]*`\]\([^)]*\)/)) { q = index(substr(text, RSTART), "]("); text = substr(text, 1, RSTART - 1) substr(text, RSTART + 1, q - 2) substr(text, RSTART + RLENGTH) }
gsub(/\[`/, "`", text); gsub(/`\]/, "`", text)
# A plain lowercase first word starts a sentence; an identifier (`pfUI`, `a.b`) is left alone.
if (match(text, /^[a-z]+/) && (substr(text, RLENGTH + 1, 1) == "" || substr(text, RLENGTH + 1, 1) ~ /^[ ;,-]$/)) text = toupper(substr(text, 1, 1)) substr(text, 2)
sub(/[.;,:]+$/, "", text)
if (text == "") text = "(see the comment)"
if (T[i] || skipfile) { skipped++; continue }
printf "%s\t%s\t%s\t%d\t%s\t%s\n", cr, module_of(rel), rel, i, text, full
}
}
BEGIN {
while ((getline s < list) > 0) tests[s] = 1
close(list)
for (a = 1; a < ARGC; a++) { skipfile = (ARGV[a] in tests); process(ARGV[a]) }
if (skipped) print "genmap: " skipped " deviation comment(s) in test code left out" > "/dev/stderr"
exit
}' > "$tmp/code.tsv"
LC_ALL=C sort -t "$(printf '\t')" -k1,1 -k2,2 -k3,3 -k4,4n "$tmp/code.tsv" > "$tmp/code.sorted"
# Stage 2: the `Deviates` rows of the CVar table. The whole file is scanned as a token stream
# (comments, strings and char literals skipped), so a call may sit on one line or twelve. A literal
# is read with Rust's `\`-newline continuation and `\"`/`\\` escapes; a constant argument
# (`crate::realmlist::CVAR_REALMLIST`) is resolved to its `const NAME: &str = "…";` in the crates.
# `cvars::tests::the_deviates_rows_are_what_the_generator_reads` pins this reading to the table.
LC_ALL=C awk '
{ src = src $0 "\n" }
function resolve(e, id, cmd, line) {
id = e; sub(/.*::/, "", id)
if (id !~ /^[A-Za-z_][A-Za-z_0-9]*$/) return "`" e "`"
cmd = "grep -rhE --include=\"*.rs\" \"const " id ": &str = \\\"\" crates 2>/dev/null"
cmd | getline line; close(cmd)
if (line ~ /= "/) { sub(/^[^"]*"/, "", line); sub(/";.*$/, "", line); return line }
return "`" e "`"
}
function str_at(i, out, c, d) { # i is on the opening quote; returns the decoded text, sets endi
out = ""; i++
while (i <= N) {
c = substr(src, i, 1)
if (c == "\"") { endi = i; return out }
if (c == "\\") {
d = substr(src, i + 1, 1)
if (d == "\n") { i += 2; while (substr(src, i, 1) ~ /[ \t\n]/) i++; continue }
if (d == "\"" || d == "\\" || d == "'"'"'") { out = out d; i += 2; continue }
if (d == "n") { out = out " "; i += 2; continue }
}
out = out c; i++
}
endi = N; return out
}
function skip_noise(i, c, d) { # returns the index past a comment, or i itself
c = substr(src, i, 1); d = substr(src, i + 1, 1)
if (c == "/" && d == "/") { while (i <= N && substr(src, i, 1) != "\n") i++; return i }
if (c == "/" && d == "*") { i += 2; while (i <= N && substr(src, i, 2) != "*/") i++; return i + 2 }
return i
}
function call(i, depth, c, k, arg, a, isstr, lit, expr, j) {
# i is just past the opening paren
depth = 1; k = 1; expr = ""; lit = 0; delete A
while (i <= N && depth > 0) {
j = skip_noise(i); if (j != i) { i = j; continue }
c = substr(src, i, 1)
if (c == "\"") { A[k] = str_at(i); lit = 1; i = endi + 1; isl[k] = 1; continue }
if (c == "(" || c == "[" || c == "{") depth++
if (c == ")" || c == "]" || c == "}") { depth--; if (depth == 0) break }
if (c == "," && depth == 1) { k++; i++; continue }
if (c !~ /[ \t\n]/) { if (!isl[k]) A[k] = A[k] c }
i++
}
for (a = 1; a <= 4; a++) {
arg = A[a]
if (!isl[a]) arg = resolve(arg)
gsub(/[\t\n]/, " ", arg)
printf "%s%s", arg, (a < 4 ? "\t" : "\n")
}
delete isl
return i
}
END {
N = length(src); i = 1; prev = ""
while (i <= N) {
j = skip_noise(i); if (j != i) { i = j; continue }
c = substr(src, i, 1)
if (c == "\"") { str_at(i); i = endi + 1; continue }
if (c == "'"'"'") {
if (substr(src, i + 2, 1) == "'"'"'") i += 3
else if (substr(src, i + 1, 1) == "\\") { i += 2; while (i <= N && substr(src, i, 1) != "'"'"'") i++; i++ }
else i++
continue
}
if (c ~ /[A-Za-z_]/) {
id = c; i++
while (i <= N && substr(src, i, 1) ~ /[A-Za-z_0-9]/) { id = id substr(src, i, 1); i++ }
if (id == "deviates" && prev != "fn" && substr(src, i, 1) == "(") i = call(i + 1)
prev = id; continue
}
i++
}
}' crates/benilla-app/src/cvars/table.rs 2>/dev/null \
| LC_ALL=C sort -f -t "$(printf '\t')" -k1,1 > "$tmp/cvars.tsv"
# Markdown-escape outside code spans (an odd backtick count means no spans: every backtick escaped).
MDESC='
function mdesc(t, cell, out, k, ch, tick, n) {
n = gsub(/`/, "`", t); out = ""; tick = 0
for (k = 1; k <= length(t); k++) {
ch = substr(t, k, 1)
if (ch == "`") { if (n % 2) out = out "\\`"; else { tick = !tick; out = out ch } continue }
if (tick) { out = out ((cell && ch == "|") ? "\\|" : ch); continue }
if (ch ~ /[\\*_<\[\]&]/ || (cell && ch == "|")) out = out "\\"
out = out ch
}
return out
}'
{
echo "# benilla — generated deviations"
echo
echo "> **GENERATED** by \`scripts/genmap.sh\` from the code — do not edit by hand; rerun the script"
echo "> when a deviation is added or reworded. No timestamp — identical code yields an identical file."
echo
echo "benilla is the 1.12.1 client reimplemented: what the reference does is what benilla does. Where it"
echo "differs on purpose, that is a deviation, the maintainer's call alone (\`docs/METHOD.md\`), and it is"
echo "written down where it lives: a setting's \`Deviates\` row in the CVar table, or a \`Deviation:\` comment"
echo "at the code, the Lua or the XML that differs. This file gathers them all, regenerated at every land,"
echo "so the list is read from the code and cannot drift from it. Test code is left out, and so are the"
echo "CVars that are benilla's own knobs with no 1.12.1 counterpart (\`ours\` rows)."
echo
echo "## Settings that ship off the reference default"
echo
echo "A setting boots at the reference's default unless it is listed here (\`docs/METHOD.md\`)."
echo
echo "| CVar | 1.12.1 | benilla | Why |"
echo "| --- | --- | --- | --- |"
LC_ALL=C awk -F '\t' "$MDESC"'
function code(v) { return "`" (v == "" ? "\"\"" : v) "`" }
{ printf "| %s | %s | %s | %s |\n", code($1), code($3), code($2), mdesc($4, 1) }' "$tmp/cvars.tsv"
echo
echo "## Deviations in the code"
echo
echo "Each entry is the first sentence of a \`Deviation:\` comment, linking to where it lives."
echo
LC_ALL=C awk -F '\t' '{ c[$1]++; if (!($1 in seen)) { seen[$1] = 1; order[++n] = $1 } }
END { for (i = 1; i <= n; i++) printf "- [%s](#%s): %d\n", order[i], order[i], c[order[i]] }' "$tmp/code.sorted"
echo
LC_ALL=C awk -F '\t' "$MDESC"'
$1 != crate { crate = $1; module = ""; printf "### %s\n\n", crate }
$2 != module { if (module != "") print ""; module = $2; printf "#### %s\n\n", module }
{ printf "- %s. ([`%s:%d`](../crates/%s/%s#L%d))\n", mdesc($5), $3, $4, $1, $3, $4 }
END { print "" }' "$tmp/code.sorted"
} > "$out/DEVIATIONS.md"
echo "genmap: wrote $out/DEVIATIONS.md ($(wc -l < "$tmp/code.sorted" | tr -d ' ') code entries, $(wc -l < "$tmp/cvars.tsv" | tr -d ' ') CVar rows)"
# ── the reason check ───────────────────────────────────────────────────────────────────────────────
# `docs/METHOD.md`: a deviation is written down with the reference fact and why we differ. So a
# `Deviation:` comment carries "because …" somewhere in its paragraph, and a CVar `Deviates` row
# carries a Why. `scripts/deviation-why.allow` is the ratchet for the comments still waiting for a
# reason: one `path<TAB>first twelve words` per line. It only shrinks. An entry that gained its reason,
# or whose comment is gone or reworded, fails until it is removed; a comment off the list without
# "because" fails; and an entry new against origin/main's copy of the list fails.
allow=scripts/deviation-why.allow
haveold=0
if git cat-file -e "origin/main:$allow" 2>/dev/null; then
haveold=1
git show "origin/main:$allow" | grep -vE '^(#|$)' > "$tmp/allow.old" || true
else
: > "$tmp/allow.old"
fi
grep -vE '^(#|$)' "$allow" 2>/dev/null > "$tmp/allow.now" || : > "$tmp/allow.now"
LC_ALL=C awk -F '\t' -v haveold="$haveold" -v oldf="$tmp/allow.old" -v nowf="$tmp/allow.now" '
function key(t, n, w, i, k) { n = split(t, w, " "); k = ""; for (i = 1; i <= n && i <= 12; i++) k = k (i > 1 ? " " : "") w[i]; return k }
BEGIN {
while ((getline l < oldf) > 0) old[l] = 1
close(oldf)
FS = "\t"
while ((getline l < nowf) > 0) { listed[l] = 1; order[++nl] = l; if (haveold == 1 && !(l in old)) { print "deviation-why: new on the allowlist, which only shrinks: " l; bad++ } }
}
FILENAME ~ /cvars\.tsv$/ { if ($4 == "") { print "deviation-why: the `" $1 "` Deviates row has no Why"; bad++ } next }
{
path = "crates/" $1 "/" $3; k = path "\t" key($6)
seen[k] = 1
if (tolower($6) ~ /(^|[^a-z])because([^a-z]|$)/) {
if (k in listed) { print "deviation-why: " path ":" $4 " has its reason now; remove it from the allowlist"; bad++ }
} else {
if (++dup[k] > 1) { print "deviation-why: " path ":" $4 " shares its first twelve words with another comment without a reason; reword one"; bad++ }
if (!(k in listed)) { print "deviation-why: " path ":" $4 " gives no reason; add \"because …\" (" substr($6, 1, 60) "…)"; bad++ }
}
}
END {
for (i = 1; i <= nl; i++) if (!(order[i] in seen)) { print "deviation-why: stale allowlist entry, remove it: " order[i]; bad++ }
if (bad) { print "deviation-why: " bad " problem(s); docs/METHOD.md: a deviation names why we differ" > "/dev/stderr"; exit 1 }
}' "$tmp/cvars.tsv" "$tmp/code.sorted" >&2 || exit 1