feat(arch): add contracts skeleton and warn-mode arch-check
Give later waves a measurable import/table/path baseline without changing business behavior or occupying the integration slot. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Executable
+151
@@ -0,0 +1,151 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user