#!/usr/bin/env bash # Category-based guard for active build, runtime, install, and launch coupling. set -euo pipefail script_root="$(cd "$(dirname "$0")/.." && pwd -P)" scan_root="$script_root" if [[ "${1:-}" == --root ]]; then [[ $# -eq 2 ]] || { echo "usage: $0 [--root PATH]" >&2; exit 2; } scan_root="$2" elif [[ $# -ne 0 ]]; then echo "usage: $0 [--root PATH]" >&2 exit 2 fi [[ -d "$scan_root" ]] || { echo "coupling scan root is not a directory: $scan_root" >&2; exit 2; } cd "$scan_root" runtime_files=() install_files=() operator_files=() contract_test_files=() add_file() { local array_name="$1" file="$2" [[ ! -f "$file" ]] || eval "$array_name+=(\"\$file\")" } for file in .dockerignore compose.yaml docker-compose.dev.yml frontend/vite.config.ts; do add_file runtime_files "$file" done if [[ -d deploy ]]; then while IFS= read -r -d '' file; do runtime_files+=("${file#./}"); done < <( find deploy -type f -print0 ) fi if [[ -d docker ]]; then while IFS= read -r -d '' file; do case "$file" in docker/session-migrate.sh|docker/cutover-legacy-sessions.sh) continue ;; esac runtime_files+=("${file#./}") done < <(find docker -type f -print0) fi for file in README.md .env.example docs/installazione-docker-4-contesti.md; do add_file install_files "$file" done if [[ -d docs/install ]]; then while IFS= read -r -d '' file; do install_files+=("${file#./}"); done < <( find docs/install -type f -print0 ) fi if [[ -d scripts ]]; then while IFS= read -r -d '' file; do case "${file#scripts/}" in test-no-deployment-coupling.sh|test-no-deployment-coupling-scope.sh) continue ;; test-*.sh) contract_test_files+=("${file#./}") continue ;; compose-with-preflight.sh|generate-connector-secrets-override.sh|unified-deployment-smoke.sh|vector-rotate-bootstrap-password.sh) continue ;; verify-*.sh) continue ;; esac operator_files+=("${file#./}") done < <(find scripts -maxdepth 1 -type f -print0) fi offenders=() scan_category() { local label="$1" pattern="$2"; shift 2 local output rg_status (($#)) || return 0 set +e output="$(rg -n -i --with-filename -- "$pattern" "$@" 2>&1)" rg_status=$? set -e case "$rg_status" in 0) while IFS= read -r match; do offenders+=("$label: $match"); done <<<"$output" ;; 1) ;; *) echo "coupling scan failed in $label (rg status $rg_status)" >&2 printf '%s\n' "$output" >&2 exit "$rg_status" ;; esac } for forbidden_file in \ deploy/compose.local-vector.yaml \ deploy/compose.preprocess-local-vector.yaml \ deploy/compose.production.yaml \ deploy/compose.psd-local.yaml.example \ deploy/compose.psd-local.yaml \ deploy/sql/20-vector-roles.sql \ deploy/vector/reconcile-roles.sh \ deploy/vector/rotate-bootstrap-password.py \ deploy/vector/secret-policy.sh \ deploy/vector/vector-db-entrypoint.sh \ scripts/bootstrap-local-psd-docker-config.sh \ scripts/local-vector-smoke.sh \ scripts/test-qwen-network-config.sh \ scripts/test-local-vector-smoke-safety.sh \ scripts/test-local-vector-smoke-live-collision.sh \ scripts/test-vector-bootstrap-rotation.sh \ scripts/test-vector-migration-image.sh \ scripts/test-vector-secret-policy.sh \ harness/tests/test_psd_local_compose_contract.py; do [[ ! -e "$forbidden_file" ]] \ || offenders+=("active filename: $forbidden_file (superseded deployment contract)") done forbidden='omics_portal|chirone|localllm_default|datamart-builder|compose\.production\.yaml|compose\.psd-local\.yaml|\bpsd\b' retired_semantic='local-vector|pgvector(_direct)?|pg_(dump|restore)|THT_VECTOR_([A-Z0-9_]+)|THT_OLLAMA_URL|THT_WS_[A-Z0-9_]*_(VECTOR|EMBEDDING)_[A-Z0-9_]+|VECTOR_API_KEY_(FILE|SOURCE)|EMBEDDING_API_KEY_(FILE|SOURCE)|vector-api-key|(^|[^A-Za-z0-9_])THT_VEC_(REST_URL|WRITE_REST_URL)|thoth_vector_http' scan_category runtime "$forbidden" "${runtime_files[@]}" scan_category install "$forbidden" "${install_files[@]}" scan_category operator "$forbidden" "${operator_files[@]}" scan_category runtime "$retired_semantic" "${runtime_files[@]}" scan_category install "$retired_semantic" "${install_files[@]}" scan_category operator "$retired_semantic" "${operator_files[@]}" # Contract tests legitimately quote forbidden names in negative assertions. Scan their positive # deployment wiring constructs instead, so a provider-owned network or retired overlay cannot be # required under a different test filename. positive_contract='networks(\?|\.)?\.?localllm_default|services(\?|\.)?\.?core(\?|\.)?\.?networks(\?|\.)?\.?localllm_default|docker compose[^\n]*(compose\.psd-local|compose\.production)|THT_PSD_[A-Z0-9_]*=' scan_category contract-test "$positive_contract" "${contract_test_files[@]}" contract_scan_files=() for file in "${contract_test_files[@]}"; do case "${file#scripts/}" in test-compose-secret-policy.sh|test-preprocess-compose-config.sh) continue ;; esac contract_scan_files+=("$file") done retired_semantic_contract='docker compose[^\n]*(compose\.local-vector|compose\.preprocess-local-vector)|THT_VECTOR_([A-Z0-9_]+)=|THT_OLLAMA_URL=|THT_WS_[A-Z0-9_]*_(VECTOR|EMBEDDING)_[A-Z0-9_]+=|vector-api-key|pgvector|pg_(dump|restore)' scan_category contract-test "$retired_semantic_contract" "${contract_scan_files[@]}" if [[ -f scripts/run-stack.sh ]]; then set +e host_pi="$(rg -n 'command -v pi|PI_BIN="pi"|PI_BIN=pi' scripts/run-stack.sh 2>&1)" host_pi_status=$? set -e case "$host_pi_status" in 0) offenders+=("operator: $host_pi") ;; 1) ;; *) echo "coupling scan failed in host-Pi contract (rg status $host_pi_status)" >&2 printf '%s\n' "$host_pi" >&2 exit "$host_pi_status" ;; esac fi if ((${#offenders[@]})); then printf '%s\n' "active deployment coupling found:" >&2 printf '%s\n' "${offenders[@]}" >&2 exit 1 fi echo "no active retired deployment or external semantic coupling found."