#!/usr/bin/env bash # Architecture boundary check. Wave 0: warn by default, never rewrite code. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" BOUNDARIES="${ROOT}/.doc/system-design/module-boundaries.yaml" MODE="warn" while [[ $# -gt 0 ]]; do case "$1" in --mode) MODE="${2:-warn}"; shift 2 ;; --mode=*) MODE="${1#--mode=}"; shift ;; *) echo "usage: $0 [--mode warn|block]" >&2; exit 2 ;; esac done if [[ "${MODE}" != "warn" && "${MODE}" != "block" ]]; then echo "mode must be warn or block" >&2 exit 2 fi violations=0 warn() { printf 'WARN %s\n' "$1" violations=$((violations + 1)) } lookup_imports() { local pkg="$1" awk -v pkg="$pkg" ' $1=="imports:" {sec=1; next} sec && $1 ~ /^(table_owners|path_owners):/ { exit } sec && $1 ~ /^[A-Za-z0-9_]+:$/ || sec && $1 ~ /^[A-Za-z0-9_]+:/ { key=$1; sub(":","",key) $1="" gsub(/^[[:space:]]+|[[:space:]]+$/,"",$0) gsub(/"/,"",$0) if (key==pkg) { print $0; exit } next } ' "${BOUNDARIES}" } lookup_owners() { local section="$1" key="$2" awk -v secname="$section" -v key="$key" ' $0 ~ "^"secname":" {sec=1; next} sec && $1 ~ /^(imports|table_owners|path_owners):/ && $1 !~ "^"secname":" { exit } sec && $1 ~ /^[A-Za-z0-9_./]+:/ { k=$1; sub(":","",k) $1="" gsub(/^[[:space:]]+|[[:space:]]+$/,"",$0) if (k==key) { print $0; exit } next } ' "${BOUNDARIES}" } allowed_has() { local needle="$1" local list="$2" [[ -z "${list}" ]] && return 1 IFS=',' read -r -a parts <<< "${list}" for p in "${parts[@]}"; do p="${p// /}" [[ "${p}" == "${needle}" ]] && return 0 done return 1 } root_pkg() { local rel="${1#chat-agent-system/internal/}" echo "${rel%%/*}" } echo "arch-check mode=${MODE}" echo "boundaries=${BOUNDARIES}" while IFS= read -r line; do [[ -z "${line}" ]] && continue pkg="$(echo "${line}" | awk '{print $1}')" root="$(root_pkg "${pkg}")" [[ "${root}" == "workflow" ]] && continue allow="$(lookup_imports "${root}")" deps="$(echo "${line}" | awk '{for(i=2;i<=NF;i++) print $i}')" for dep in ${deps}; do [[ "${dep}" != chat-agent-system/internal/* ]] && continue droot="$(root_pkg "${dep}")" [[ "${droot}" == "${root}" ]] && continue if ! allowed_has "${droot}" "${allow}"; then warn "R-import ${root} -> ${droot} not in baseline allowlist" fi done if [[ "${root}" == "contracts" ]]; then for dep in ${deps}; do if [[ "${dep}" == chat-agent-system/internal/* && "${dep}" != chat-agent-system/internal/contracts ]]; then warn "R1 contracts imports ${dep}" fi done fi done < <(go list -f '{{.ImportPath}} {{join .Imports " "}}' ./internal/...) declare -A seen_table while IFS= read -r table; do owners="$(lookup_owners table_owners "${table}")" while IFS= read -r file; do [[ -z "${file}" ]] && continue rel="${file#${ROOT}/internal/}" pkg="${rel%%/*}" key="${table}|${pkg}" [[ -n "${seen_table[$key]+x}" ]] && continue seen_table[$key]=1 if ! allowed_has "${pkg}" "${owners}"; then warn "R5 table ${table} mentioned in ${pkg} (target owner ${owners})" fi done < <(rg -l --glob '*.go' --glob '!*_test.go' --fixed-strings "${table}" "${ROOT}/internal" || true) done <<'TABLES' document_records chat_messages chat_turns freeform_documents task_runs TABLES declare -A seen_path while IFS= read -r lit; do owners="$(lookup_owners path_owners "${lit}")" while IFS= read -r file; do [[ -z "${file}" ]] && continue rel="${file#${ROOT}/internal/}" pkg="${rel%%/*}" key="${lit}|${pkg}" [[ -n "${seen_path[$key]+x}" ]] && continue seen_path[$key]=1 if ! allowed_has "${pkg}" "${owners}"; then warn "R6 path ${lit} mentioned in ${pkg} (target owner ${owners})" fi done < <(rg -l --glob '*.go' --glob '!*_test.go' --fixed-strings "${lit}" "${ROOT}/internal" || true) done <<'PATHS' output.md data/dev workspaces/ PATHS echo "violations=${violations}" if [[ "${MODE}" == "block" && "${violations}" -gt 0 ]]; then echo "arch-check block: failing" >&2 exit 1 fi echo "arch-check ${MODE}: recorded ${violations} baseline findings" exit 0