Files
Dicklesworthstone 5f05938484 feat(pages,doctor,status,indexer,sources,gate): pages key CLI, WAL hygiene, segment pressure, setup sync, install fall-through, docs validator, and end-to-end proofs for #439/#440/#441 (coding_agent_session_search-ctigq, -2w1sc, -hr9rn, -q2vn9, -z2uon, -wpe6s, -vg2e1, -tcrxi, -c2xxq, -xwuon, -jwox6, -vdyxd, -apriu)
Correction to 1f642a24's message: the golden leak was not caused by the golden
harness's own stale-on-read refresh — verify7 reproduced it with that opt-out
in place. The writer is tests/cli_robot.rs: 52 call sites use
tests/fixtures/search_demo_data IN PLACE through a base_cmd() that never
disabled auto-refresh, and on a fleet worker the checkout is not under the OS
temp dir, so a robot-CLI search spawned `cass index --background` into the
committed fixture and ingested that worker's real sessions before the golden
tests copied it. base_cmd() now sets CASS_AUTO_REFRESH=0. Proof: verify9 ran
the in-place robot tests before golden regeneration in one job and
search_robot/stats_full_payload came back byte-identical to HEAD.

pages key (ctigq): `cass pages key list|add-password|add-recovery|revoke|
rotate --archive <bundle>` over the complete key_management engine (3,915
lines, zero callers until now). Passwords via prompt or --password-stdin
(current on line 1, new on line 2), never argv; --json; exit 1 engine refused,
2 usage, 3 not a bundle, 6 password required. Four unit tests on a real
encrypted bundle plus a CLI suite (tests/pages_key_cli.rs). docs/RECOVERY.md
rewritten to the real surface; README row.

WAL hygiene (z2uon): forget --apply, dedup --apply and analytics validate
--fix close through the index run's checkpointing close; doctor `archive_wal`
(pass ≤ 64 MiB; warn with size and remedy; warn-and-defer under an index run;
--fix checkpoints and reports pass / blocked / failed truthfully) with a
sparse-WAL e2e in tests/cli_doctor.rs. status/health e2e for db_bytes /
wal_bytes / shm_present, now on the `status` surface too (q2vn9).

Segment pressure (wpe6s, GH #441): quill_bridge::segment_file_count
(metadata-only upper bound), status/health `index.segment_files`, doctor
`index_segments` (warn above 8× the merge threshold, `cass index --full`
remedy), CASS_TEST_SKIP_POST_RUN_LEXICAL_MAINTENANCE hook and an e2e that
builds a 40-segment generation, sees the warning, and proves a plain
`cass index` consolidates it.

GH #439 (vg2e1) and GH #440 (tcrxi) end-to-end proofs: FTS-repair hooks
(sleep per page after the heartbeat; park before the first page) — a slow
repair survives a 20 s abort window, a parked one exits 70 with the
`index-stalled` envelope; a hook parks a force-rebuild between the staged
engine commit and the checkpoint write, records the gap, and a plain
`cass index` resumes with exit 0 and no duplicate identities.

sources (vdyxd, G.1): setup no longer marks the final sync complete without
running it — interactive setup runs `cass sources sync` afterwards and records
it only on success; --json setup defers it and reports sync.status=pending.
Remote install walks candidate_methods() (binstall → verified prebuilt → cargo
install → bootstrap) and falls through on failure instead of stopping at the
first choice. `cass sources sync --all` exists (jwox6): doctor, robot-docs and
the fleet rehearsal recommended it while clap rejected it.

installer (apriu, G.2a): install.sh probes host glibc and falls back to
build-from-source below 2.38; proven on ubuntu:22.04 (2.35 → source route)
and ubuntu:24.04 (2.39 → prebuilt).

docs validator (xwuon, A.9): scripts/validate_docs.sh --keys/--flags/--env/
--truth check README key bindings against the TUI key map (123/123, planted
fake key fails), every `cass … --flag` usage against `cass … --help`, and
every README env var against the code; found and fixed `sources sync --all`,
`timeline --days`, the `<mark>` highlight claim and a dead CASS_UI_METRICS
row. gate.sh gains an opt-in docs-truth stage.

gate (c2xxq): scripts/gate.sh had a false green — verify8 was cut by the
fleet's 30-minute SSH ceiling after fmt+clippy and the script still said
GREEN. Now a non-zero rch exit is RED, every expected stage (plus a terminal
job-complete marker) must report, the receipt captures both streams and
survives a RED run, --lib-only/--no-goldens split runs under the ceiling, the
lib stage runs under `timeout`, and the fleet target dir no longer inherits
the shell's CARGO_TARGET_DIR.

contract (hr9rn): introspect/capabilities has_json_output looks through the
subcommand tree (11 commands flip to true and gain the robot-format arg).
TUI: the unreachable `Alt+W => swarm cockpit` arm removed. CHANGELOG
[Unreleased], AGENTS swarm/issue rules, plan §7b progress log.

Verification (fleet, scripts/gate.sh STAGE receipts, RUST_MIN_STACK=16777216):
- verify9 (2026-09-02 00:35-00:58): fmt 0, clippy 0, goldens-regen 0,
  goldens 0 with search_robot/stats_full_payload byte-identical to HEAD
  after the in-place cli_robot tests ran first (leak fix proven);
  cli_index/cli_robot/cli_status 101 (Codex fixture names, thresholds,
  health nesting) fixed in tree and re-run below.
- verify10 (01:04-01:19): `cargo test --lib` to completion, 6,847 passed /
  6 failed / 38 ignored, no wedge; the six (bookmarks missing from
  CANONICAL_TOP_LEVEL_COMMANDS, four sqlite3-CLI tests on a worker without
  sqlite3, daemon socket path over sun_path on a long TMPDIR) fixed in
  tree and re-run below.
- verify11 (05:08-05:23): fmt 0, clippy 0, lib-tests 0 (canonical
  commands, daemon, sqlite, sources install/setup, pages key,
  quill_bridge, stall diagnostics, fingerprint cache), pages_key_cli 0,
  cli_robot 0, cli_doctor 0, tui_flows 0, bookmarks_cli 0, goldens-regen
  0, goldens 0; cli_index 101 (gh439 reads the last stdout JSON line now)
  and cli_status 101 (health nests state.database) fixed in tree.
- verify12 (05:41-05:47, full lib suite on this tree): fmt 0, clippy 0,
  lib-tests 0 — 6,855 passed / 0 failed / 38 ignored in 144 s.
- verify13 (05:47-05:55, --regen-goldens): fmt 0, clippy 0, lib-tests 0
  (quill_bridge, stall diagnostics), cli_status 0, cli_doctor 0,
  pages_key_cli 0, goldens-regen 0, goldens 0; cli_index 101 because the
  gh439 slow-repair test parsed the last LINE of a pretty-printed summary
  (harness bug) - it now stream-parses the last JSON document.
- verify14 (05:57-06:00, cli_index gh439 after that fix): fmt 0, clippy 0,
  test-cli_index 0 (gate: GREEN).
Goldens: 12 reviewed diffs (segment_files / db_bytes / wal_bytes /
shm_present fields, has_json_output flips, pages key and sources sync
--all in the command trees, doctor archive_wal / index_segments checks);
the four that differ only by the regeneration writer's dropped final
newline were left at HEAD.

Measurement (owner's archive: 10.3 GB db, 200 MB WAL, 267 Quill segment
files; `search "borrow checker lifetimes" --robot --robot-meta --limit 5`,
five warm runs, CASS_AUTO_REFRESH=0): v0.7.1 median wall 15.3 s (search_ms
6.3-7.5 s, other_ms 1.0-1.9 s); this tree (release build) median wall
1.9 s (search_ms 337-406 ms, other_ms 754-810 ms after a first-run 2.6 s
sidecar miss). hits=1 on both. Not yet isolated: the engine-time drop
happened with the segment count unchanged, so the per-open seal in the old
Quill config is the likelier cause than fragmentation on this archive.
Consolidation of the owner's 267 segments was NOT achieved: the documented
path (`cass index`, and `cass doctor --fix` for the WAL) never leaves the
frankensqlite writable open on that archive - one core, 6.7 GB RSS,
~30k 24-byte header reads/s on the 200 MB WAL, no progress in 7 min -
while read-only opens take 1.5 s. On a COPY whose WAL C SQLite checkpointed
losslessly (1,012 conversations / 538,807 messages before and after), the
same `cass index` gets through the open and indexes, so the WAL is the
trigger; C SQLite's quick_check on that copy also reports a doubly-
referenced freelist page and free-space corruption on ~35 pages that
`cass doctor` calls healthy. The archive's last completed index is
2026-08-14. Recorded as a new P0 in the plan (section 7b); the live
archive was not modified.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 06:18:55 -04:00

678 lines
23 KiB
Bash
Executable File

#!/usr/bin/env bash
# Documentation validation script for cass.
#
# Validates:
# - Link validity in markdown files
# - Required sections in README
# - CLI help text consistency
# - Example code validity
#
# Usage:
# ./scripts/validate_docs.sh # Run all validations
# ./scripts/validate_docs.sh --links # Only check links
# ./scripts/validate_docs.sh --readme # Only check README
# ./scripts/validate_docs.sh --help # Only check CLI help
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
RCH_BIN="${RCH_BIN:-rch}"
RCH_TARGET_DIR="${RCH_TARGET_DIR:-${TMPDIR:-/tmp}/rch_target_cass_validate_docs}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Counters
ERRORS=0
WARNINGS=0
CHECKS=0
# =============================================================================
# Helper Functions
# =============================================================================
log_pass() {
((CHECKS += 1))
echo -e "${GREEN}${NC} $1"
}
log_fail() {
((CHECKS += 1))
((ERRORS += 1))
echo -e "${RED}${NC} $1"
}
log_warn() {
((WARNINGS += 1))
echo -e "${YELLOW}!${NC} $1"
}
log_info() {
echo -e " $1"
}
ensure_rch() {
if ! command -v "$RCH_BIN" &> /dev/null; then
log_fail "rch binary not found; validate_docs cargo work must be offloaded"
return 1
fi
}
run_cargo() {
"$RCH_BIN" exec -- env CARGO_TARGET_DIR="$RCH_TARGET_DIR" cargo "$@"
}
section() {
echo ""
echo "═══════════════════════════════════════════════════════════════"
echo " $1"
echo "═══════════════════════════════════════════════════════════════"
}
# =============================================================================
# Link Validation
# =============================================================================
check_links() {
section "Link Validation"
local md_files
md_files=$(find . -name "*.md" -not -path "./target/*" -not -path "./.git/*" 2>/dev/null || true)
if [[ -z "$md_files" ]]; then
log_warn "No markdown files found"
return
fi
local file
for file in $md_files; do
log_info "Checking $file..."
# Check for broken internal links (relative paths)
local links
links=$(grep -oE '\[([^]]+)\]\(([^)]+)\)' "$file" 2>/dev/null | grep -v 'http' | grep -v 'mailto' || true)
while IFS= read -r link; do
[[ -z "$link" ]] && continue
# Extract the path from the link
local path
path=$(echo "$link" | sed -E 's/.*\]\(([^)#]+).*/\1/')
# Skip anchors and empty paths
[[ -z "$path" || "$path" == "#"* ]] && continue
# Resolve relative to file directory
local dir
dir=$(dirname "$file")
local full_path="$dir/$path"
if [[ ! -e "$full_path" && ! -e "$path" ]]; then
log_fail "Broken link in $file: $path"
fi
done <<< "$links"
# Check for valid URL patterns in external links
local urls
urls=$(grep -oE 'https?://[^)"\s>]+' "$file" 2>/dev/null || true)
while IFS= read -r url; do
[[ -z "$url" ]] && continue
# Basic URL format validation
if ! echo "$url" | grep -qE '^https?://[a-zA-Z0-9]'; then
log_fail "Malformed URL in $file: $url"
fi
done <<< "$urls"
done
log_pass "Link validation complete"
}
# =============================================================================
# README Validation
# =============================================================================
check_readme() {
section "README Validation"
local readme="README.md"
if [[ ! -f "$readme" ]]; then
log_fail "README.md not found"
return
fi
log_info "Checking required sections..."
# Check for key sections
local sections=("installation" "usage" "features" "license")
for sec in "${sections[@]}"; do
if grep -qi "## .*$sec\|# .*$sec" "$readme"; then
log_pass "README has $sec section"
else
log_warn "README may be missing $sec section"
fi
done
# Check for examples
if grep -q '```' "$readme"; then
log_pass "README contains code examples"
else
log_warn "README has no code examples"
fi
# Check for badges (optional)
if grep -qE '!\[.*\]\(https?://' "$readme"; then
log_pass "README has badges/images"
else
log_info "README has no badges (optional)"
fi
# Check file isn't empty or too short
local lines
lines=$(wc -l < "$readme")
if [[ "$lines" -lt 20 ]]; then
log_warn "README seems short ($lines lines)"
else
log_pass "README has adequate content ($lines lines)"
fi
}
# =============================================================================
# CLI Help Validation
# =============================================================================
check_help() {
section "CLI Help Validation"
# Check if binary exists
local binary="$RCH_TARGET_DIR/release/cass"
if [[ ! -x "$binary" ]]; then
binary="$RCH_TARGET_DIR/debug/cass"
fi
if [[ ! -x "$binary" ]]; then
binary="target/release/cass"
fi
if [[ ! -x "$binary" ]]; then
binary="target/debug/cass"
fi
if [[ ! -x "$binary" ]]; then
log_warn "cass binary not found, building through rch..."
ensure_rch || return
run_cargo build --quiet --bin cass 2>/dev/null || {
log_fail "Could not build cass binary"
return
}
binary="$RCH_TARGET_DIR/debug/cass"
if [[ ! -x "$binary" ]]; then
log_fail "Built cass binary not found at $binary"
return
fi
fi
log_info "Using binary: $binary"
# Test --help
if "$binary" --help &>/dev/null; then
log_pass "--help flag works"
else
log_fail "--help flag failed"
fi
# Test -h
if "$binary" -h &>/dev/null; then
log_pass "-h flag works"
else
log_fail "-h flag failed"
fi
# Test --version
local version_output
version_output=$("$binary" --version 2>&1 || true)
if echo "$version_output" | grep -qE '[0-9]+\.[0-9]+\.[0-9]+'; then
log_pass "--version shows version number"
else
log_fail "--version doesn't show version number"
fi
# Test subcommand help
local subcommands=("search" "index" "export" "tui" "health")
for cmd in "${subcommands[@]}"; do
if "$binary" "$cmd" --help &>/dev/null; then
log_pass "Subcommand '$cmd' has help"
else
log_warn "Subcommand '$cmd' help unavailable"
fi
done
# Check help mentions key features
local help_output
help_output=$("$binary" --help 2>&1 || true)
if echo "$help_output" | grep -qi "search"; then
log_pass "Help mentions search"
else
log_warn "Help doesn't mention search"
fi
if echo "$help_output" | grep -qi "index"; then
log_pass "Help mentions index"
else
log_warn "Help doesn't mention index"
fi
}
# =============================================================================
# Security Doc Validation
# =============================================================================
check_security() {
section "Security Documentation"
local security="SECURITY.md"
if [[ ! -f "$security" ]]; then
log_warn "SECURITY.md not found (may be generated at publish time)"
return
fi
log_info "Checking security documentation..."
# Check for key security concepts
local concepts=("encrypt" "argon" "aes" "password" "key")
for concept in "${concepts[@]}"; do
if grep -qi "$concept" "$security"; then
log_pass "Security doc mentions $concept"
else
log_warn "Security doc may not cover $concept"
fi
done
}
# =============================================================================
# Example Code Validation
# =============================================================================
# =============================================================================
# README ↔ code truth checks (reality check 2026-09-01, WS-A.9)
#
# The README is the vision document; these checks make its concrete claims
# executable: every key binding in a README key table must have a matching arm
# in the TUI key map, every `cass` flag the README shows must exist in
# `cass introspect --json`, and every env var in the README env table must be
# one `cass robot-docs env` knows. `README_PATH` overrides the file under test
# (used to plant a negative); `CASS_BIN` overrides the binary.
# =============================================================================
resolve_cass_binary() {
local candidate
for candidate in "${CASS_BIN:-}" "$RCH_TARGET_DIR/release/cass" "$RCH_TARGET_DIR/debug/cass" \
"target/release/cass" "target/debug/cass"; do
if [[ -n "$candidate" && -x "$candidate" ]]; then
echo "$candidate"
return 0
fi
done
if command -v cass &> /dev/null; then
command -v cass
return 0
fi
return 1
}
# Translate one README key token (`Alt+Shift+W`, `F4`, `Ctrl+Del`, `Esc`, `?`)
# into an extended regex over the TUI key map source, or print nothing when the
# token is not a key (prose in the key column).
key_token_pattern() {
local token="$1" mods="" base
local ctrl=0 alt=0 shift=0
base="$token"
while [[ "$base" == *+* && ${#base} -gt 1 ]]; do
case "${base%%+*}" in
Ctrl|ctrl) ctrl=1 ;;
Alt|alt|Meta|Opt|Option) alt=1 ;;
Shift|shift) shift=1 ;;
*) break ;;
esac
base="${base#*+}"
done
local code=""
case "$base" in
F[0-9]|F1[0-2]) code="KeyCode::F\\(${base#F}\\)" ;;
Esc|Escape) code="KeyCode::Esc" ;;
Enter|Return) code="KeyCode::Enter" ;;
Tab) if [[ $shift == 1 ]]; then code="KeyCode::BackTab"; shift=0; else code="KeyCode::Tab"; fi ;;
BackTab) code="KeyCode::BackTab" ;;
Del|Delete) code="KeyCode::Delete" ;;
Backspace) code="KeyCode::Backspace" ;;
Up|Down|Left|Right|Home|End) code="KeyCode::${base}" ;;
PageUp|PgUp) code="KeyCode::PageUp" ;;
PageDown|PgDn) code="KeyCode::PageDown" ;;
Space) code="Char\\(' '\\)" ;;
[1-9]) # README writes digit ranges as `Alt+1`..`Alt+9`; the map has one
# range arm: `KeyCode::Char(c @ '1'..='9') if <modifier>`.
code="(Char\\('${base}'\\)|Char\\(c @ '1'\\.\\.='9'\\))" ;;
?) # single printable character: either case is accepted, and either
# of the two forms the TUI uses — a `KeyCode::Char('x')` arm, or a
# typed-character re-dispatch `text == "x"` (the detail pane).
local lower upper escaped
lower=$(printf '%s' "$base" | tr '[:upper:]' '[:lower:]')
upper=$(printf '%s' "$base" | tr '[:lower:]' '[:upper:]')
case "$base" in
[A-Za-z]) code="(Char\\('(${lower}|${upper})'\\)|text == \"(${lower}|${upper})\")" ;;
"'") code="Char\\('\\\\''\\)" ;;
*)
escaped=$(printf '%s' "$base" | sed 's/[][\\.*^$/?+(){}|]/\\&/g')
code="(Char\\('${escaped}'\\)|text == \"${escaped}\")"
;;
esac
;;
*) return 0 ;;
esac
[[ $ctrl == 1 ]] && mods="${mods}(?=.*ctrl)"
[[ $alt == 1 ]] && mods="${mods}(?=.*alt)"
[[ $shift == 1 ]] && mods="${mods}(?=.*shift)"
# PCRE: the arm line must mention the code and every modifier guard.
echo "^(?=.*${code})${mods}"
}
check_keys() {
section "README key bindings ↔ TUI key map"
local readme="${README_PATH:-README.md}" keymap="src/ui/app.rs"
local missing=0 checked=0 token pattern cell
while IFS= read -r cell; do
# Split multi-key cells: `F3` / `Alt+G`
while IFS= read -r token; do
token="${token#\`}"; token="${token%\`}"
[[ -z "$token" ]] && continue
pattern=$(key_token_pattern "$token")
[[ -z "$pattern" ]] && continue
checked=$((checked + 1))
if ! grep -P -q "$pattern" "$keymap"; then
log_fail "README key \`$token\` has no matching arm in $keymap"
missing=$((missing + 1))
fi
done < <(printf '%s\n' "$cell" | grep -o -E '`[^`]+`')
done < <(grep -E '^\| `[^|]+` *\|' "$readme" | sed -E 's/^\| ([^|]*)\|.*/\1/' | grep -E '`(F[0-9]+|Esc|Enter|Tab|Del|Delete|Backspace|Up|Down|Left|Right|Home|End|Page(Up|Down)|Space|Ctrl|Alt|Shift|.)`|\+' )
if [[ $missing -eq 0 ]]; then
log_pass "all $checked README key bindings resolve to a TUI key-map arm"
fi
}
check_flags() {
section "README cass flags ↔ cass introspect"
local readme="${README_PATH:-README.md}" binary known
if ! binary=$(resolve_cass_binary); then
log_warn "cass binary not found (set CASS_BIN); skipping flag check"
return
fi
# `cass introspect --json` lists top-level commands only (nested
# subcommands such as `sources sync` are not described there — a contract
# gap noted in the reality check), so the truth source for a README
# invocation is clap itself: `cass <command path> --help`. Help output is
# collected once per command path.
local global_flags
global_flags=$("$binary" --help 2>/dev/null | grep -o -E -- '--[a-z][a-z0-9-]+' | sort -u)
if [[ -z "$global_flags" ]]; then
log_fail "cass --help printed no flags from $binary"
return
fi
declare -A help_cache=()
flags_for_path() {
local path="$1"
if [[ -z "${help_cache[$path]+x}" ]]; then
# shellcheck disable=SC2086
help_cache[$path]=$("$binary" $path --help 2>/dev/null | grep -o -E -- '--[a-z][a-z0-9-]+' | sort -u)
fi
printf '%s\n' "${help_cache[$path]}"
}
local missing=0 checked=0 line path token flag
# README lines that deliberately show WRONG spellings (the auto-correction
# tables) are not claims about the contract. A line may hold several
# invocations; each `cass …` segment is judged on its own.
while IFS= read -r line; do
path=""
local first=1
for token in $(printf '%s' "$line"); do
if [[ $first == 1 ]]; then first=0; continue; fi # the literal `cass`
case "$token" in
-*|\"*|\'*|\`*|\||'#'*) break ;;
*)
if [[ "$token" =~ ^[a-z][a-z0-9-]*$ ]]; then
path="${path:+$path }$token"
else
break
fi
;;
esac
done
while IFS= read -r flag; do
[[ -z "$flag" ]] && continue
case "$flag" in --help|--version) continue ;; esac
checked=$((checked + 1))
if printf '%s\n' "$global_flags" | grep -q -x -- "$flag"; then
continue
fi
if [[ -z "$path" ]]; then
# `cass --robot` in prose names a flag without its command;
# not checkable against one --help, so it is a warning.
log_warn "README mentions \`cass $flag\` without a command; cannot check it against --help"
continue
fi
if flags_for_path "$path" | grep -q -x -- "$flag"; then
continue
fi
log_fail "README shows \`cass $path $flag\` but \`cass $path --help\` does not list $flag"
missing=$((missing + 1))
done < <(printf '%s\n' "$line" | grep -o -E -- '--[a-z][a-z0-9-]+' | sort -u)
done < <(grep -E '(^|[^a-z])cass ' "$readme" \
| grep -v -i -E 'typo|alias|corrected|auto-correct|Levenshtein|wrong|converted|become|normalized|promoted|route' \
| sed -E 's/(^|[^a-z])cass /\ncass /g' | grep -E '^cass ' \
| sed -E 's/`.*$//') # an inline code span ends at the next backtick
if [[ $missing -eq 0 ]]; then
log_pass "all $checked README cass flag usages are accepted by the matching \`cass … --help\`"
fi
}
check_env() {
section "README env vars ↔ code"
local readme="${README_PATH:-README.md}" binary robot_known=""
# Truth source: the variable must be read somewhere in src/ (dotenvy::var,
# env::var, or a named constant). `cass robot-docs env` is a curated subset
# for agents, so a README variable it omits is only a warning.
if binary=$(resolve_cass_binary); then
robot_known=$("$binary" robot-docs env 2>/dev/null | grep -o -E '^\s+[A-Z][A-Z0-9_]+' | tr -d ' ' | sort -u)
fi
# Connector root overrides (`CASS_<AGENT>_DATA_ROOT`) are read by the
# franken_agent_detection crate, not by src/; look in its checkout or the
# cargo registry copy when present.
local -a env_roots=(src)
local fad fad_found=0
for fad in /data/projects/franken_agent_detection/src \
"${CARGO_HOME:-$HOME/.cargo}"/registry/src/*/franken_agent_detection-*/src; do
if [[ -d "$fad" ]]; then
env_roots+=("$fad")
fad_found=1
fi
done
local missing=0 checked=0 unlisted=0 var
while IFS= read -r var; do
checked=$((checked + 1))
if ! grep -r -q -F -- "\"$var\"" "${env_roots[@]}" --include='*.rs'; then
if [[ $fad_found == 0 && "$var" == CASS_*_DATA_ROOT ]]; then
# Connector root overrides live in the detection crate; without
# its source on this host the claim cannot be checked.
log_warn "README env var \`$var\` is a connector root override; the agent-detection crate source is not available here to confirm it"
continue
fi
log_fail "README env table documents \`$var\` but nothing in src/ (or the agent-detection crate) reads \"$var\""
missing=$((missing + 1))
elif [[ -n "$robot_known" ]] && ! printf '%s\n' "$robot_known" | grep -q -x -- "$var"; then
unlisted=$((unlisted + 1))
fi
done < <(grep -E '^\| `CASS_[A-Z0-9_]+`' "$readme" | grep -o -E 'CASS_[A-Z0-9_]+' | sort -u)
if [[ $missing -eq 0 ]]; then
log_pass "all $checked README env vars are read by the code"
fi
if [[ $unlisted -gt 0 ]]; then
log_warn "$unlisted README env vars are not in the curated \`cass robot-docs env\` list"
fi
}
check_examples() {
section "Example Code Validation"
# Extract code blocks from README
local readme="README.md"
if [[ ! -f "$readme" ]]; then
log_warn "README.md not found"
return
fi
# Check for shell examples
if grep -qE '```(bash|sh|shell)' "$readme"; then
log_pass "README has shell examples"
else
log_info "No shell examples in README"
fi
# Check for Rust examples
if grep -qE '```rust' "$readme"; then
log_pass "README has Rust examples"
else
log_info "No Rust examples in README"
fi
# Validate cargo commands mentioned work
local cargo_cmds
cargo_cmds=$(grep -oE 'cargo (build|test|run|install|bench)[^`]*' "$readme" 2>/dev/null | head -5 || true)
if [[ -n "$cargo_cmds" ]]; then
log_info "Found cargo commands in README"
while IFS= read -r cmd; do
[[ -z "$cmd" ]] && continue
log_info " - $cmd"
done <<< "$cargo_cmds"
fi
}
# =============================================================================
# Cargo Doc Validation
# =============================================================================
check_cargo_docs() {
section "Cargo Documentation"
log_info "Building documentation..."
ensure_rch || return
if run_cargo doc --no-deps --quiet 2>/dev/null; then
log_pass "cargo doc builds successfully"
else
log_fail "cargo doc has errors"
fi
# Check for documentation warnings
local doc_output
doc_output=$(run_cargo doc --no-deps 2>&1 || true)
local missing_docs
missing_docs=$(echo "$doc_output" | grep -c "missing documentation" || true)
if [[ "$missing_docs" -gt 0 ]]; then
log_warn "$missing_docs items missing documentation"
else
log_pass "No missing documentation warnings"
fi
}
# =============================================================================
# Main
# =============================================================================
main() {
echo "╔═══════════════════════════════════════════════════════════════╗"
echo "║ CASS Documentation Validation ║"
echo "╚═══════════════════════════════════════════════════════════════╝"
cd "$PROJECT_ROOT"
case "${1:-all}" in
--links)
check_links
;;
--readme)
check_readme
;;
--help)
check_help
;;
--security)
check_security
;;
--examples)
check_examples
;;
--cargo)
check_cargo_docs
;;
--keys)
check_keys
;;
--flags)
check_flags
;;
--env)
check_env
;;
--truth)
check_keys
check_flags
check_env
;;
all|*)
check_readme
check_links
check_help
check_security
check_examples
check_cargo_docs
check_keys
check_flags
check_env
;;
esac
# Summary
section "Summary"
echo ""
echo " Checks: $CHECKS"
echo " Passed: $((CHECKS - ERRORS))"
echo " Errors: $ERRORS"
echo " Warnings: $WARNINGS"
echo ""
if [[ "$ERRORS" -gt 0 ]]; then
echo -e "${RED}Documentation validation failed with $ERRORS error(s)${NC}"
exit 1
elif [[ "$WARNINGS" -gt 0 ]]; then
echo -e "${YELLOW}Documentation validation passed with $WARNINGS warning(s)${NC}"
exit 0
else
echo -e "${GREEN}Documentation validation passed!${NC}"
exit 0
fi
}
main "$@"