#!/usr/bin/env bash # Verify the public installation and workspace documentation against the current topology. set -euo pipefail if [[ -n "${THT_DOCS_VERIFY_ROOT:-}" ]]; then root="$(cd "$THT_DOCS_VERIFY_ROOT" && pwd -P)" else root="$(cd "$(dirname "$0")/.." && pwd -P)" fi fail() { echo "verify-workspace-install-docs.sh: $*" >&2 exit 1 } require_file() { [[ -f "$root/$1" ]] || fail "missing required file: $1" } require_text() { local path="$1" text="$2" grep -Fq -- "$text" "$root/$path" || fail "$path must contain: $text" } forbid_text() { local path="$1" text="$2" if grep -Fq -- "$text" "$root/$path"; then fail "$path retains obsolete reference: $text" fi } verify_compose_topology() { python3 - "$root/compose.yaml" <<'PY' import pathlib import sys import yaml compose = yaml.safe_load(pathlib.Path(sys.argv[1]).read_text()) expected = { "catalog-db", "catalog-migrate", "core", "embedding", "embedding-model-init", "frontend", "qdrant", "workspace-maintenance", } services = compose.get("services", {}) if set(services) != expected: raise SystemExit(f"compose.yaml services mismatch: {sorted(services)}") if services["catalog-migrate"].get("profiles") != ["catalog-maintenance"]: raise SystemExit("catalog-migrate must remain an explicit catalog-maintenance operation") if services["workspace-maintenance"].get("profiles") != ["workspace-maintenance"]: raise SystemExit("workspace-maintenance must remain profile-gated") if services["catalog-migrate"].get("depends_on", {}).get("catalog-db", {}).get("condition") != "service_healthy": raise SystemExit("catalog-migrate must wait for healthy catalog-db") PY require_text scripts/run-stack.sh 'up -d catalog-db' require_text scripts/run-stack.sh 'run --rm catalog-migrate' echo "Compose topology contract passed" } verify_navigation() { local path for path in \ install/first-start.md \ install/standalone-manual-it.md \ install/standalone-manual-en.md \ operations/workspaces.md \ operations/database-management.md \ guida-utente.md \ product-overview.md \ install/shell-and-language.md; do require_file "docs/$path" require_text mkdocs.yml "$path" done echo "Current documentation navigation contract passed" } verify_standalone_installation_guides() { for guide in docs/install/standalone-manual-it.md docs/install/standalone-manual-en.md; do require_file "$guide" for text in \ 'git clone https://git.tylconsulting.it/mptyl/ThothII.git' \ 'scripts/check-standalone-prerequisites.sh' \ 'scripts/install-tht.sh' \ 'tht setup --profile local --shell-mode full --shell-default-locale en' \ 'scripts/verify-standalone-install.sh' \ 'Docker Hub' \ 'Gate A' \ 'Gate B'; do require_text "$guide" "$text" done done require_file scripts/check-standalone-prerequisites.sh require_file scripts/verify-standalone-install.sh echo "Manual standalone installation guides contract passed" } verify_install_and_workspace_guides() { local install='docs/install/first-start.md' local workspace='docs/operations/workspaces.md' require_file "$install" require_file "$workspace" for text in \ 'tht setup --profile local' \ '--configure-only' \ 'catalog-migrate' \ 'tht --installation /absolute/path/thothii-installation.yaml doctor --json'; do require_text "$install" "$text" done for text in \ 'workspace inspect' \ 'workspace preprocess run' \ 'database in **Database Management**' \ 'write-only runtime secrets' \ 'Runtime sessions support direct PostgreSQL and REST bindings.'; do require_text "$workspace" "$text" done for obsolete in \ 'docs/install/local-workspace-registry.md' \ 'docs/install/server-workspace-registry.md' \ 'docs/install/local.md' \ 'docs/install/server.md'; do forbid_text README.md "$obsolete" forbid_text "$install" "$obsolete" forbid_text "$workspace" "$obsolete" done echo "Local installation and workspace operations contract passed" } verify_examples() { require_file docs/install/examples/thothii-installation.local.yaml require_file docs/install/examples/thothii-installation.server.yaml require_file docs/install/examples/workspace-bindings.env.example python3 - "$root/docs/install/examples/thothii-installation.local.yaml" \ "$root/docs/install/examples/thothii-installation.server.yaml" \ "$root/docs/install/examples/workspace-bindings.env.example" <<'PY' import pathlib import re import sys import yaml local = yaml.safe_load(pathlib.Path(sys.argv[1]).read_text()) server = yaml.safe_load(pathlib.Path(sys.argv[2]).read_text()) bindings = pathlib.Path(sys.argv[3]).read_text() if local.get("profile") != "local" or server.get("profile") != "server": raise SystemExit("installation examples must retain their local/server profiles") for document in (local, server): catalog = document.get("modelCatalog") if not isinstance(catalog, dict) or "providers" not in catalog or "authentication" not in document: raise SystemExit("installation example lacks model catalog or authentication configuration") if re.search(r"(?:KEY|PASSWORD|TOKEN|SECRET)=[^\n#<][^\n]*", bindings): raise SystemExit("workspace bindings example contains a secret value") PY echo "Installation examples contract passed" } verify_all() { verify_compose_topology verify_navigation verify_standalone_installation_guides verify_install_and_workspace_guides verify_examples } case "${1:-}" in --fixtures-only) [[ $# -eq 1 ]] || fail 'usage: verify-workspace-install-docs.sh --fixtures-only | --profile {local|server}' verify_all ;; --profile) [[ $# -eq 2 && "${2:-}" =~ ^(local|server)$ ]] \ || fail 'usage: verify-workspace-install-docs.sh --fixtures-only | --profile {local|server}' verify_all echo "$2 installation documentation verification passed" ;; *) fail 'usage: verify-workspace-install-docs.sh --fixtures-only | --profile {local|server}' ;; esac