Files
serverAppConfigs/tests/docs_completeness.bats
T
Frank 3880503057 feat(scripts): OKF conformance linter, trash-aware vault seams
Add scripts/check-okf.py: enforces the OKF v0.1 hard rules the repo had
been carrying by convention — parseable frontmatter with type/title/
description/timestamp, description shape (length, not-equals-title, no
restated quantity), the reserved-name and index.md rules, log.md's date
heading shape (bare ISO date, one entry per date, newest first), the
500-line hint, and the doubled-page signature (a second `type:` key in
the body). Citations are reported as a soft tally, not a failure. The
500-line ceiling is escapable two ways, both explainable: `okf-exempt:
length` in a page's own frontmatter, and pages derived as one-per-
endpoint — live endpoints from scripts/, retired-endpoint era pages from
their tag (AGENTS.md lets a campaign ledger live on the page that
measured it).

scripts/gen-vault-index.py: sibling-presence was tested as a bare stem
substring, so a page whose name is a prefix of another page's name
(`frank-medium` inside `frank-medium-rocmfpx`) silently never got an
index row. Test the wikilink form instead.

scripts/check-vault-links.py: `.AGENTTRASH` is deleted-but-held state,
not vault content. It is now excluded from both the scan set and the
target index, so a live link into the trash reads as unresolved instead
of resolved.

tests/docs_completeness.bats: the same exclusion in both page walks —
trashed pages need no index row, and a trashed page must not satisfy the
docs requirement for a live compose service. Adds the OKF conformance
and index-freshness tests.

scripts/port-census.py: the port-before-cut orphan census used to prove
a merge lost no fact, kept so the next collapse can re-run it.
2026-10-19 04:54:06 -07:00

111 lines
5.3 KiB
Bash

#!/usr/bin/env bats
# Seam: documentation completeness.
# Every service defined in a compose.yaml must have a matching documentation
# page in serverAppConfigs-docs/ and must be reachable from the index. This is the "docs first"
# contract of this repo: no service ships without documentation.
# The vault's second law, added 2026-10-18: ONE page per live LLM endpoint.
# A payload swap is an Eras row on that endpoint's page, never a page of its
# own -- a second file holding the same shape is the next thing to go stale.
load helpers
@test "every compose service has a docs page" {
while IFS= read -r f; do
while IFS= read -r svc; do
# service name is the docs slug: RomM -> serverAppConfigs-docs/Dankserver/RomM.md
# matched case-insensitively (vault uses Title Case)
local doc
doc=$(find "$REPO_ROOT/serverAppConfigs-docs" -iname "${svc}.md" -not -path '*/.AGENTTRASH/*' 2>/dev/null | head -1)
if [ -z "$doc" ]; then
echo "service '$svc' (from $f) has no docs page: expected serverAppConfigs-docs/**/${svc}.md"
return 1
fi
done < <(services_in "$f")
done < <(find "$REPO_ROOT" -name 'compose.yaml' -not -path '*/.git/*' | sort)
}
@test "docs index links every docs page" {
local index="$REPO_ROOT/serverAppConfigs-docs/index.md"
[ -f "$index" ] || { echo "missing $index"; return 1; }
while IFS= read -r page; do
local base
base=$(basename "$page" .md)
grep -qF "[[$base]]" "$index" || {
echo "docs page $base is not linked from $index"
return 1
}
# `.AGENTTRASH` is deleted-but-held state per the repo's deletion policy, not
# vault content: trashed pages need no index row, and a trashed page must not
# answer as the docs page for a live compose service.
done < <(find "$REPO_ROOT/serverAppConfigs-docs" -name '*.md' -not -name 'index.md' -not -path '*/.obsidian/*' -not -path '*/.AGENTTRASH/*' | sort)
}
@test "no unresolved links in the docs vault" {
# A wikilink to a page that does not exist is a promise the repo cannot
# keep. Fixing the three that were dangling (2026-09-16) also meant
# retiring a reference to dankclaw/compose.yaml, which no longer exists.
# The checker models Obsidian's resolution rules; see its docstring for
# the three false-positive classes a naive version produces.
run "$REPO_ROOT/scripts/check-vault-links.py" "$REPO_ROOT/serverAppConfigs-docs"
[ "$status" -eq 0 ] || { echo "$output"; false; }
}
@test "one page per live LLM endpoint" {
# The live set is derived, not hardcoded: an endpoint is live if scripts/
# holds both its env and its unit (frank-tts has an env but no unit, so it
# is not an inference endpoint; homepage-status is the reverse).
local vault="$REPO_ROOT/serverAppConfigs-docs" derived=0 name extra
for envf in "$REPO_ROOT"/scripts/frank-*.env; do
name=$(basename "$envf" .env)
[ -f "$REPO_ROOT/scripts/$name.service" ] || continue
derived=$((derived + 1))
[ -f "$vault/LLM/$name.md" ] || {
echo "live endpoint '$name' has no canonical page: serverAppConfigs-docs/LLM/$name.md"
return 1
}
extra=$(find "$vault" -name "${name}-*.md" -not -path '*/.AGENTTRASH/*' | sort)
if [ -n "$extra" ]; then
echo "endpoint '$name' owns exactly one page; these files re-create a second owner of its shape:"
echo "$extra"
echo "Fold them into LLM/$name.md (an Eras row plus, if needed, a Law) and trash the page."
return 1
fi
done
# Guard against the derivation going vacuous -- an empty loop passes silently.
[ "$derived" -ge 3 ] || {
echo "expected at least 3 live endpoints derived from scripts/*.env + scripts/*.service, got $derived"
return 1
}
}
@test "the vault conforms to OKF v0.1" {
# Frontmatter contract (type/description/timestamp), the 500-line split
# hint, log.md's date-heading shape and newest-first order, and index rows
# that restate a fleet quantity instead of pointing at its owner. The only
# length exemptions are the live endpoint pages, derived from scripts/ --
# see scripts/check-okf.py.
run "$REPO_ROOT/scripts/check-okf.py" "$REPO_ROOT/serverAppConfigs-docs"
[ "$status" -eq 0 ] || { echo "$output"; false; }
}
@test "vault indices are generated, not hand-carved" {
# serverAppConfigs-docs/index.md is regenerated from page frontmatter and
# every sub-index is synced against its siblings. A hand-edited row is how
# a page silently leaves the index, so regeneration must be a no-op. The
# vault is copied first: the seam never mutates the checkout.
local work="$BATS_TEST_TMPDIR/vault"
cp -r "$REPO_ROOT/serverAppConfigs-docs" "$work"
run "$REPO_ROOT/scripts/gen-vault-index.py" "$work"
[ "$status" -eq 0 ] || { echo "$output"; false; }
local stale=0 f rel
while IFS= read -r f; do
rel=${f#"$REPO_ROOT"/serverAppConfigs-docs/}
if ! diff -q "$f" "$work/$rel" >/dev/null; then
echo "stale index row(s) in $f -- run scripts/gen-vault-index.py"
diff "$f" "$work/$rel" | head -20
stale=1
fi
done < <(find "$REPO_ROOT/serverAppConfigs-docs" -name index.md -not -path '*/.AGENTTRASH/*' | sort)
[ "$stale" -eq 0 ]
}