Files
khosra/scripts/verify.sh
T
bdeshiandClaude Opus 5 ec6e9838f0 rename docs/ to harness/, and reserve docs/ for the reader
docs/content-model.md opens with "Engine specification". It is also where the
rule lives that a leading underscore makes a file unaddressable — and the human
who owns this site did not know that rule, because nothing in this repository is
addressed to an author. Twelve documents named docs/ while being exclusively
about building the parser is a signpost pointing at the wrong room.

Naming the directory for its audience makes the gap visible instead of hiding it.
docs/ is now reserved and deliberately absent: an empty docs/ is an honest
statement that end-user documentation does not exist, where docs/ full of parser
specs was a claim that it did.

HARNESS.md stays at the root. Root holds the three entry points — README.md for a
human, CLAUDE.md for an agent, HARNESS.md for whoever maintains the machine — and
harness/README.md is the map of the directory, so moving the guide inside would
have collided with it for nothing.

Mechanical and wide: 100 path references across 24 files. Every verify.sh gate
that names a doc by path, the directory lists the dangling-path and ADR-number
gates scan, surface.sh's output target, the Makefile, CLAUDE.md's read order, the
skill, four commands, and two Go package comments. A first pass with a shell loop
silently edited only four files and the rest still said docs/; the fix was to
write the file list out and check the remaining count was zero rather than trust
the loop's exit status.

No rule, threshold, gate or obligation moved — this is a rename, and the gates
demonstrated it twice: they stayed green on the new paths, and the ADR-number gate
caught ADR-0082 before the entry existed.

Deferred, both on the human's call: the end-user documentation site itself, which
wants its own decision about where it lives and whether its claims are gated; and
moving examples/ under docs/, since demo-site is a live site root that verify.sh,
the coverage test and make demo all point at, and moving it would couple a rename
to a design nobody has made.

31 files, +146/-106.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:11:06 +06:00

495 lines
24 KiB
Bash
Executable File

#!/usr/bin/env bash
# The objective gate. No opinions, only exit codes.
# Usage: ./scripts/verify.sh [--quiet|--list]
set -uo pipefail
# --list answers "does this gate actually exist?" without reading the script. Docs that claim a gate are
# checkable against it. The pattern is written pas[s] so this grep does not match its own source line.
if [ "${1:-}" = "--list" ]; then
printf 'gates in %s:\n' "$0"
grep -oE 'pas[s] "[^"]+"' "$0" | awk -F'"' '{print $2}' |
sed 's/\$([^)]*)/…/g; s/\$[A-Za-z_][A-Za-z_0-9]*/…/g; s/^/ /' | sort -u
exit 0
fi
cd "$(dirname "$0")/.." || exit 1
[ -f scripts/budgets.env ] && . scripts/budgets.env
CORE_LOC_MAX=${CORE_LOC_MAX:-2000}
EXT_LOC_MAX=${EXT_LOC_MAX:-2000}
FILE_LOC_WARN=${FILE_LOC_WARN:-500}
FUNC_LOC_WARN=${FUNC_LOC_WARN:-60}
DEPS_MAX=${DEPS_MAX:-6}
CLAUDE_LOC_MAX=${CLAUDE_LOC_MAX:-150}
fail=0
warn=0
# --quiet prints only what needs acting on: failures, warnings, and the verdict. A green run is two
# lines instead of forty-three, which matters because this is the most-run command in the repo and its
# output is read far more often by an agent's context window than by a person. Passing gate names are
# never information — `--list` answers "does this gate exist?" and does it without running anything.
quiet=0
[ "${1:-}" = "--quiet" ] && quiet=1
pass() { [ "$quiet" -eq 1 ] || printf ' ok %s\n' "$1"; }
bad() { printf ' FAIL %s\n' "$1"; fail=1; }
note() { printf ' warn %s\n' "$1"; warn=$((warn + 1)); }
head_() { [ "$quiet" -eq 1 ] || printf '\n%s\n' "$1"; }
if ! command -v go >/dev/null 2>&1 || ! command -v gofmt >/dev/null 2>&1; then
echo "FAIL go toolchain not found on PATH — cannot verify anything"
exit 1
fi
# --- documentation coupling -------------------------------------------------
# Runs before the Go gates so it still applies while the repo is harness-only.
# Code and the docs describing it move together, or they drift apart silently.
head_ "documentation"
if [ -d .git ] && command -v git >/dev/null 2>&1; then
# -uall matters: the default collapses untracked directories to "dir/", which would hide a
# brand-new internal/ext/feeds/feeds.go from the .go check below.
changed=$(git status --porcelain -uall 2>/dev/null | sed 's/^...//' | sed 's/.* -> //')
if [ -z "$changed" ]; then
pass "working tree clean — nothing to couple"
else
if echo "$changed" | grep -qE '\.go$' && ! echo "$changed" | grep -qx 'harness/state.md'; then
bad "*.go changed but harness/state.md did not — inventory, counters and latent items move with the code"
else
pass "code/state.md coupling"
fi
if echo "$changed" | grep -qE '^(cmd|internal)/.*\.go$' && ! echo "$changed" | grep -qE '_test\.go$'; then
# A comment-only or gofmt-only diff ships no behaviour, so it owes no test. Compare the added and
# removed lines with comments, blanks and indentation stripped: equal sets mean nothing happened.
gochanged=$(echo "$changed" | grep -E '^(cmd|internal)/.*\.go$')
godiff=$(git diff HEAD -- $gochanged 2>/dev/null)
# strip the marker, comments, blanks, and *all* whitespace runs — gofmt realignment then compares
# equal, while any real edit still differs.
strip() { grep -E "^[$1]" | grep -vE '^(\+\+\+|---)' | sed "s/^[$1]//" |
grep -vE '^[[:space:]]*(//|/\*|\*/|\* )' | tr -s ' \t' ' ' |
sed 's/^ //; s/ $//' | grep -v '^$' | sort; }
newgo=$(git ls-files --others --exclude-standard -- $gochanged 2>/dev/null)
if [ -z "$newgo" ] && [ "$(echo "$godiff" | strip '+')" = "$(echo "$godiff" | strip '-')" ]; then
pass "code/test coupling (comment- or format-only)"
else
bad "cmd/ or internal/ .go changed but no _test.go did — behaviour ships with a test (conventions.md)"
fi
else
pass "code/test coupling"
fi
if echo "$changed" | grep -qE '^internal/.*templates/.*\.html$' && ! echo "$changed" | grep -qx 'harness/theme-contract.md'; then
bad "embedded templates changed but harness/theme-contract.md did not — they drift together (ADR-0023)"
else
pass "templates/theme-contract coupling"
fi
# One exemption, and it is about the agent tooling rather than this project: .claude/settings.json is
# Claude Code's own permission list, which HARNESS.md calls a convenience, so editing it explains
# nothing about khosra. khosra's own configuration is not exempt from anything — the engine's settings
# live in the site root (content-model.md) and the engine validates them itself. Everything else under
# .claude/, and all of scripts/ including budgets.env, changes how the machine behaves and is gated.
harnesschanged=$(echo "$changed" | grep -E '^(CLAUDE\.md$|scripts/|\.claude/)' | grep -v '^\.claude/settings\.json$' || true)
if [ -n "$harnesschanged" ] && ! echo "$changed" | grep -qx 'HARNESS.md'; then
bad "the harness changed (CLAUDE.md, scripts/ or .claude/) but HARNESS.md did not — the guide to the machine is part of the machine"
else
pass "harness/HARNESS.md coupling"
fi
fi
# The reference theme is a contract demonstration, not a design: zero JavaScript (ADR-0026).
themefiles=$(find internal -path '*templates*' -name '*.html' 2>/dev/null || true)
if [ -n "$themefiles" ]; then
scripted=$(echo "$themefiles" | xargs grep -ln '<script' 2>/dev/null || true)
if [ -n "$scripted" ]; then
bad "<script> in the reference theme — it is a contract demonstration, not a design (ADR-0026): $(echo "$scripted" | tr '\n' ' ')"
else
pass "reference theme is script-free"
fi
fi
# Invariant 2, as mechanism rather than memory. Content from the site root is trusted, so raw HTML renders
# there (ADR-0060) — but the *trusted* half is the whole claim, and this is what holds it to one place.
# A second call site means some other input has been handed the pipeline that trusts its author, which is
# the failure ADR-0003 exists to prevent; when comments arrive they get their own goldmark without it.
# Matched by file rather than by line, so the rule can still be explained next to the code it governs.
unsafe=$(grep -rl 'html\.WithUnsafe()' --include='*.go' cmd internal 2>/dev/null | sort || true)
if [ "$unsafe" != "internal/render/render.go" ]; then
bad "html.WithUnsafe belongs in internal/render/render.go and nowhere else (ADR-0003, ADR-0060); found in: $(echo "${unsafe:-nothing}" | tr '\n' ' ')"
else
pass "raw HTML is enabled in exactly one renderer"
fi
# The example site is a demonstration that has to stay true (ADR-0051). `check` over it catches the class of
# rot a test cannot: a content rule that changed while the example kept the old shape. Findings are allowed —
# the example carries one on purpose — but nothing fatal is.
if [ -d examples/demo-site ]; then
if go run ./cmd/khosra check -site examples/demo-site >/tmp/example-check.log 2>&1; then
pass "the example site still checks out"
else
bad "examples/demo-site has a fatal finding — the demo must stay a site the engine accepts"
sed 's/^/ /' /tmp/example-check.log
fi
fi
# Dangling references. Every one of these found a real stale pointer when run by hand.
# `.scratch/` is included only when it exists: it is gitignored, so on a fresh clone the handoff is
# legitimately absent (harness/README.md says "if present") and checking it there would fail correct
# work. Once the directory is present, a named file missing from it is a stale pointer like any
# other — which is how state.md's pointer to a deleted build-queue outlived it by two commits. That
# path is named without backticks on purpose: this gate reads its own file, so the example would be a
# reference, and the gate would flag the comment explaining it.
scratchpat=""
[ -d .scratch ] && scratchpat='|\.scratch'
refs=$(grep -rhoE "\`(harness|scripts|ideas|reference|\.claude$scratchpat)/[A-Za-z0-9_./-]+\`" \
harness CLAUDE.md HARNESS.md ideas reference .claude scripts 2>/dev/null | tr -d '`' | sort -u)
dangling=""
for f in $refs; do [ -e "$f" ] || dangling="$dangling $f"; done
[ -n "$dangling" ] && bad "reference to a path that does not exist:$dangling"
adrs=$(grep -rhoE 'ADR-[0-9]{4}' harness CLAUDE.md HARNESS.md ideas reference .claude scripts cmd internal 2>/dev/null | sort -u)
missingadr=""
for a in $adrs; do
# the log registers every number ever used — as an entry, or in the withdrawn line
grep -q "$a" harness/decisions.md 2>/dev/null || missingadr="$missingadr $a"
done
[ -n "$missingadr" ] && bad "reference to an ADR with no entry in decisions.md:$missingadr"
secs=$(grep -rhoE 'CLAUDE\.md`? §[0-9]+' harness HARNESS.md ideas reference .claude scripts 2>/dev/null |
grep -oE '§[0-9]+' | tr -d '§' | sort -u)
missingsec=""
for s in $secs; do
grep -q "^## $s\. " CLAUDE.md || missingsec="$missingsec §$s"
done
[ -n "$missingsec" ] && bad "reference to a CLAUDE.md section that does not exist:$missingsec"
[ -z "$dangling$missingadr$missingsec" ] && pass "references resolve"
# Index rot: a folder of files nobody lists is a folder nobody reads.
for d in ideas reference; do
[ -d "$d" ] || continue
files=$(find "$d" -name '*.md' -not -name 'README.md' | wc -l | tr -d ' ')
listed=$(grep -c '^- \[' "$d/README.md" 2>/dev/null || true)
if [ "$files" != "${listed:-0}" ]; then
note "$d: $files file(s), $listed index line(s) — index out of date"
fi
if [ "$d" = "ideas" ]; then
unstatused=$(grep -L '^Status:' ideas/*.md 2>/dev/null | grep -v 'README.md' || true)
[ -n "$unstatused" ] && note "ideas: no Status line: $(echo "$unstatused" | tr '\n' ' ')"
fi
done
# True staleness: was state.md updated no earlier than the last commit that touched Go?
#
# Compared, never declared. A hand-written sha cannot do this job: a commit cannot name itself, so the
# doc update had to trail the code in a commit of its own, and folding the two together then left the
# sha naming a commit that no longer existed (ADR-0057).
lastgo=$(git log -1 --format=%H -- '*.go' 2>/dev/null)
laststate=$(git log -1 --format=%H -- harness/state.md 2>/dev/null)
if [ -n "$lastgo" ]; then
if [ -n "$laststate" ] && git merge-base --is-ancestor "$lastgo" "$laststate" 2>/dev/null; then
pass "harness/state.md is current with the code"
else
note "harness/state.md was last updated in ${laststate:-no commit}, older than the .go change in $(git log -1 --format=%h -- '*.go')"
fi
fi
# Every counter row says what does *not* count (ADR-0070). Four counters were re-scoped on first contact,
# each time by naming something that had been wrongly included, so the column is where that thinking has to
# happen — before the counter is trusted rather than after it misleads. Checked by shape, not by wording: a
# row of the counters table must have five filled cells.
counters=$(awk -F'|' '/^\| Counter \| Now \|/{on=1; next} on && /^\|---/{next} on && /^\|/{
name=$2; gsub(/^[ \t]+|[ \t]+$/, "", name);
last=$6; gsub(/^[ \t]+|[ \t]+$/, "", last);
if (last == "") print name
} on && !/^\|/{on=0}' harness/state.md)
if [ -n "$counters" ]; then
bad "counter row with no 'does not count' (ADR-0070): $(echo "$counters" | tr '\n' ';')"
else
pass "every counter row says what does not count"
fi
# The generated surface is only worth reading if it cannot be wrong. Regenerate and compare rather
# than trusting that whoever added a function also ran the script.
if [ -x scripts/surface.sh ]; then
if ! scripts/surface.sh | cmp -s - harness/surface.md; then
bad "harness/surface.md is stale — run 'make surface'"
else
pass "harness/surface.md matches the code"
fi
fi
# CLAUDE.md is re-sent on every single turn, which makes it the most expensive file per byte in the
# repo. A ceiling here is not tidiness; it is the only budget that bills per turn.
claudeloc=$(wc -l <CLAUDE.md | tr -d ' ')
if [ "$claudeloc" -gt "$CLAUDE_LOC_MAX" ]; then
bad "CLAUDE.md is $claudeloc lines, over CLAUDE_LOC_MAX=$CLAUDE_LOC_MAX (move detail to the owning doc)"
else
pass "CLAUDE.md $claudeloc / $CLAUDE_LOC_MAX lines"
fi
else
note "not a git repo — doc coupling unenforceable"
fi
result_and_exit() {
head_ "result"
if [ "$fail" -eq 0 ]; then
printf ' PASS %s warning(s)\n' "$warn"
else
printf ' FAIL fix the above before reporting success\n'
fi
[ "$quiet" -eq 1 ] || printf '\n'
exit "$fail"
}
if [ ! -f go.mod ]; then
head_ "go"
note "no go.mod — nothing to verify yet. Run 'go mod init' as part of the first feature."
result_and_exit
fi
mod=$(awk '/^module /{print $2; exit}' go.mod)
# Engine source only. The site root is outside this repo (ADR-0011); ideas/ and reference/ may hold
# exploratory scratch code, which is not the engine and is never formatted, vetted, built or budgeted.
gofiles=$(find . -name '*.go' -not -path './vendor/*' -not -path './.git/*' \
-not -path './ideas/*' -not -path './reference/*' -not -path './.scratch/*' 2>/dev/null)
if [ -z "$gofiles" ]; then
head_ "go"
note "go.mod present but no .go files yet — nothing to verify."
result_and_exit
fi
head_ "correctness"
pkgs=$(go list ./... 2>/dev/null | grep -vE "^$mod/(ideas|reference|\.scratch)(/|$)")
if [ -z "$pkgs" ]; then
head_ "go"; note "no engine packages yet — nothing to verify."
result_and_exit
fi
unformatted=$(echo "$gofiles" | xargs gofmt -l 2>/dev/null || true)
if [ -n "$unformatted" ]; then bad "gofmt: $(echo "$unformatted" | tr '\n' ' ')"; else pass "gofmt"; fi
if go vet $pkgs >/tmp/vet.log 2>&1; then pass "go vet"; else bad "go vet"; sed 's/^/ /' /tmp/vet.log; fi
if go build $pkgs >/tmp/build.log 2>&1; then pass "go build"; else bad "go build"; sed 's/^/ /' /tmp/build.log; fi
# The working tree building says nothing about what is being committed. Staging a subset — a changed
# signature without its caller — lands a commit that does not compile, which `git bisect` then trips over
# forever. So when something is staged, build the staged tree itself, from a throwaway checkout of the index
# so nothing here can touch the real one.
if ! git diff --cached --quiet 2>/dev/null; then
staged=$(mktemp -d)
if git archive "$(git write-tree)" 2>/dev/null | tar -x -C "$staged" 2>/dev/null &&
(cd "$staged" && go build ./... >/tmp/staged-build.log 2>&1); then
pass "the staged tree builds"
else
bad "what is staged does not build on its own — a partial stage, most likely a caller left behind"
sed 's/^/ /' /tmp/staged-build.log 2>/dev/null
fi
rm -rf "$staged"
fi
if echo "$gofiles" | grep -q '_test\.go$'; then
if go test -race $pkgs >/tmp/test.log 2>&1; then
pass "go test ($(grep -c '^ok' /tmp/test.log) packages)"
else
bad "go test"; sed 's/^/ /' /tmp/test.log
fi
else
note "no tests exist yet"
fi
head_ "dependencies"
reqs=$(awk '
/^require[ \t]*\(/ { inb = 1; next }
inb && /^\)/ { inb = 0; next }
{
ln = $0
ind = (ln ~ /\/\/[ \t]*indirect/) ? "indirect" : "direct"
sub(/\/\/.*/, "", ln)
gsub(/^[ \t]+|[ \t]+$/, "", ln)
if (ln == "") next
if (!inb) { if (ln !~ /^require[ \t]/) next; sub(/^require[ \t]+/, "", ln) }
split(ln, a, /[ \t]+/)
if (a[1] != "") print a[1], ind
}' go.mod)
direct=$(echo "$reqs" | awk '$2=="direct" {print $1}' | grep -v '^$' || true)
total=$(echo "$reqs" | grep -cv '^$' || true)
if [ -f scripts/allowed-deps.txt ]; then
# the file documents "# starts a comment", so strip trailing ones too, not just whole-line
allowed=$(sed 's/#.*//' scripts/allowed-deps.txt | sed 's/[[:space:]]*$//' | grep -v '^$' || true)
unlisted=""
for d in $direct; do
echo "$allowed" | grep -qxF "$d" || unlisted="$unlisted $d"
done
if [ -n "$unlisted" ]; then
bad "dependency not on allowlist:$unlisted (needs an ADR + scripts/allowed-deps.txt)"
else
directcount=$(echo "$direct" | grep -c . || true)
pass "allowlist ($directcount direct)"
fi
fi
# An untidy go.mod misreports what is direct, so the allowlist check above would silently skip a
# dependency added by `go get` before anything imported it.
if go mod tidy -diff >/tmp/tidy.log 2>&1; then
pass "go.mod is tidy"
else
bad "go.mod is untidy — run go mod tidy; until then a direct dependency can hide as indirect"
fi
if [ "$total" -gt "$DEPS_MAX" ]; then
bad "module count $total exceeds DEPS_MAX=$DEPS_MAX"
else
pass "module count $total / $DEPS_MAX"
fi
head_ "architecture"
# The layering in conventions.md, enforced. Dependencies point inward; a sibling import here is
# what turns a layered engine into a ball of mud, and it always looks locally reasonable.
# One line per package: "importpath imp1 imp2 …". go list is authoritative; grepping source is not.
pkgimports=$(go list -f '{{.ImportPath}}{{range .Imports}} {{.}}{{end}}' ./... 2>/dev/null)
pairs=$(echo "$pkgimports" | awk '{for (i=2; i<=NF; i++) print $1, $i}')
layer() { # $1 = importing layer, $2..$n = layers it may not import
local from="$1"; shift
for to in "$@"; do
echo "$pairs" | awk -v f="$mod/internal/$from" -v t="$mod/internal/$to" \
'index($1,f)==1 && index($2,t)==1 {print $1" → "$2}'
done
}
violations=$(
layer content render web
layer render web
echo "$pairs" | awk -v c="$mod/cmd" 'index($2,c)==1 && index($1,c)!=1 {print $1" → "$2}'
)
# Invariant 7, as a rule rather than a list: a feature is a leaf, so `cmd/` is the *only* thing that may
# import one. Stated positively, this also covers a core package that does not exist yet — the enumeration
# above cannot, and the day someone adds `internal/foo` it would import `internal/ext` unnoticed. Sibling
# imports between features fail here too: they are what make an agent's read set compound (ADR-0027), and
# they are the other way a feature stops being deletable. Test files are excluded by `go list .Imports`,
# which is right — the demo's own test exercises features on purpose (ADR-0069).
siblings=$(echo "$pairs" | awk -v e="$mod/internal/ext/" -v c="$mod/cmd" '
index($2,e)==1 && index($1,c)!=1 {print $1" → "$2}')
if [ -n "$violations$siblings" ]; then
bad "import boundary violated (conventions.md layering): $(echo "$violations$siblings" | tr '\n' ' ')"
else
pass "import boundaries"
fi
nopkgdoc=""
for dir in $(echo "$gofiles" | grep -v '_test\.go$' | xargs -n1 dirname 2>/dev/null | sort -u); do
documented=$(awk 'prev ~ /^\/\// && /^package /{print FILENAME} {prev=$0}' "$dir"/*.go 2>/dev/null | head -1)
[ -z "$documented" ] && nopkgdoc="$nopkgdoc $dir"
done
if [ -n "$nopkgdoc" ]; then
bad "package without a package comment (conventions.md Documentation):$nopkgdoc"
else
pass "every package documented"
fi
undocumented=$(echo "$gofiles" | grep -v '_test\.go$' | xargs awk '
/^(func|type|var|const) [A-Z]/ { if (prev !~ /^\/\//) printf "%s:%d ", FILENAME, FNR }
{ prev = $0 }' 2>/dev/null)
if [ -n "$undocumented" ]; then
note "exported identifier without a doc comment: $undocumented"
else
pass "exported identifiers documented"
fi
missingdoc=""
for dir in $(echo "$gofiles" | grep '^\./internal/ext/' | xargs -n1 dirname 2>/dev/null | sort -u); do
[ -f "$dir/doc.go" ] || missingdoc="$missingdoc $dir"
done
if [ -n "$missingdoc" ]; then
bad "internal/ext package without doc.go — contributes / cascade keys / contract fields / not doing (ADR-0027):$missingdoc"
else
pass "every feature has a doc.go"
fi
head_ "budgets"
# Two trees, two ceilings: the core must stop growing after Arc 2, ext is where growth belongs.
loc() { [ -z "$1" ] && { echo 0; return; }; echo "$1" | xargs wc -l 2>/dev/null | awk '$2!="total"{t+=$1} END{print t+0}'; }
nontest=$(echo "$gofiles" | grep -v '_test\.go$' || true)
extfiles=$(echo "$nontest" | grep '^\./internal/ext/' || true)
corefiles=$(echo "$nontest" | grep -v '^\./internal/ext/' || true)
coreloc=$(loc "$corefiles")
extloc=$(loc "$extfiles")
if [ "$coreloc" -gt "$CORE_LOC_MAX" ]; then
bad "core is $coreloc lines, over CORE_LOC_MAX=$CORE_LOC_MAX (shrink it, or raise it in an ADR)"
else
pass "core $coreloc / $CORE_LOC_MAX lines"
fi
if [ "$extloc" -gt "$EXT_LOC_MAX" ]; then
bad "internal/ext is $extloc lines, over EXT_LOC_MAX=$EXT_LOC_MAX (a template may have done it)"
else
pass "ext $extloc / $EXT_LOC_MAX lines"
fi
oversized=$(echo "$gofiles" | xargs wc -l 2>/dev/null | awk -v m="$FILE_LOC_WARN" '$2!="total" && $1>m {print $2"("$1")"}')
if [ -n "$oversized" ]; then
note "files over FILE_LOC_WARN=$FILE_LOC_WARN: $(echo "$oversized" | tr '\n' ' ')"
else
pass "no file over $FILE_LOC_WARN lines"
fi
longfuncs=$(echo "$gofiles" | xargs awk -v m="$FUNC_LOC_WARN" '
/^func / { start = FNR; name = $0; inf = 1; next }
inf && /^}/ { if (FNR - start > m) printf "%s:%d(%d)\n", FILENAME, start, FNR - start; inf = 0 }
' 2>/dev/null)
[ -n "$longfuncs" ] && note "functions over $FUNC_LOC_WARN lines: $(echo "$longfuncs" | tr '\n' ' ')"
if [ -f Dockerfile ] && [ "${VERIFY_DOCKER:-0}" = "1" ]; then
head_ "container (VERIFY_DOCKER=1)"
if command -v docker >/dev/null 2>&1; then
if docker build -q -t khosra:verify . >/tmp/docker.log 2>&1; then
pass "docker build"
else
bad "docker build"; tail -20 /tmp/docker.log | sed 's/^/ /'
fi
else
note "docker not on PATH"
fi
fi
head_ "style floor"
# conventions.md states these absolutely, so they fail. A rule enforced as a suggestion teaches
# the agent to read every rule as a suggestion.
badpkg=$(find ./cmd ./internal -type d 2>/dev/null | grep -Ei '/(utils?|helpers?|common|shared|misc|manager|base|impl|core)$' || true)
if [ -n "$badpkg" ]; then bad "package name says nothing (CLAUDE.md rule 3.6): $(echo "$badpkg" | tr '\n' ' ')"; else pass "package names"; fi
inits=$(echo "$gofiles" | xargs grep -ln '^func init()' 2>/dev/null || true)
if [ -n "$inits" ]; then bad "init() found — wire explicitly in cmd/: $(echo "$inits" | tr '\n' ' ')"; else pass "no init()"; fi
badlog=$(echo "$pairs" | awk '$2=="log" {print $1}' | sort -u)
if [ -n "$badlog" ]; then bad "imports log, not log/slog: $(echo "$badlog" | tr '\n' ' ')"; else pass "log/slog only"; fi
clocks=$(echo "$gofiles" | grep -v '_test\.go$' | grep -v '/clock\.go$' | xargs grep -ln 'time\.Now(' 2>/dev/null || true)
if [ -n "$clocks" ]; then bad "time.Now() outside a clock.go — a render that reads the clock is only true for a while (conventions.md): $(echo "$clocks" | tr '\n' ' ')"; else pass "clock accessed through clock.go"; fi
panics=$(echo "$gofiles" | grep -v '_test\.go$' | grep -v '^\./cmd/' | xargs grep -ln 'panic(' 2>/dev/null || true)
if [ -n "$panics" ]; then bad "panic() outside cmd/ — request-time failure degrades (conventions.md): $(echo "$panics" | tr '\n' ' ')"; else pass "no panic outside cmd"; fi
head_ "smells (advisory)"
# Only calls that actually wrap: fmt.Errorf without an err argument is creating an error, not losing one.
nowrap=$(echo "$gofiles" | xargs grep -hn 'fmt\.Errorf(' 2>/dev/null |
grep -v '%w' | grep -cE '\berr\b' || true)
[ "${nowrap:-0}" -gt 0 ] && note "$nowrap fmt.Errorf without %w (wrap at package boundaries)"
# Only exported signatures: conventions.md bans interface{} as an API escape hatch, but ADR-0002 mandates
# an open page object, so unexported code reading Extra legitimately takes any and always will.
anyuse=$(echo "$gofiles" | grep -v '_test\.go$' |
xargs grep -HnE '^func (\([^)]*\) )?[A-Z][A-Za-z0-9_]*\(.*(\bany\b|interface\{\})' 2>/dev/null |
cut -d: -f1 | sort -u || true)
[ -n "$anyuse" ] && note "interface{} or any present (no empty interface for flexibility): $(echo "$anyuse" | tr '\n' ' ')"
deep=$(echo "$gofiles" | xargs awk '/^\t\t\t\t\t[^\t}]/ {print FILENAME; nextfile}' 2>/dev/null | sort -u || true)
[ -n "$deep" ] && note "nesting past 4 (conventions.md): $(echo "$deep" | tr '\n' ' ')"
deadexp=$(echo "$gofiles" | grep '^\./internal/' | grep -v '_test\.go$' | xargs grep -hoE '^func [A-Z][A-Za-z0-9_]*' 2>/dev/null | awk '{print $2}' | sort -u |
while read -r sym; do
n=$(echo "$gofiles" | xargs grep -c "\b$sym\b" 2>/dev/null | awk -F: '{s+=$2} END{print s+0}')
[ "${n:-0}" -le 1 ] && printf '%s ' "$sym"
done)
[ -n "$deadexp" ] && note "exported but referenced once — unexport or delete: $deadexp"
todos=$(echo "$gofiles" | xargs grep -c 'TODO\|FIXME\|XXX' 2>/dev/null | awk -F: '{s+=$2} END{print s+0}')
[ "${todos:-0}" -gt 0 ] && note "$todos TODO/FIXME markers (latent items belong in harness/state.md)"
result_and_exit