Files
lucky-cat/tools/check_architecture_guards.sh
T

172 lines
11 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
fail() {
echo "[FAIL] $1" >&2
exit 1
}
echo "[CHECK] facade duplicate guards"
if rg -n "domain_digital_input_config_t" components/domain/include/domain.h >/dev/null 2>&1; then
fail "domain facade must not expose platform-owned digital input config"
fi
if rg -n "domain_runtime_stepper_|domain_runtime_platform_init\(" components/domain/include/domain.h >/dev/null 2>&1; then
fail "domain facade must not expose platform primitive runtime adapters"
fi
if rg -n "domain_runtime_storage_" components/domain/include/domain.h >/dev/null 2>&1; then
fail "domain facade must not expose generic platform storage adapters"
fi
if rg -n "domain_runtime_lcd_trigger_slot_animation" components/domain/include/domain.h components/domain/src >/dev/null 2>&1; then
fail "domain must request LCD slot reels through the expression catalog"
fi
if rg -n "play_prompt_named|platform_behavior_play_prompt_named" components/platform/include/platform.h components/platform/internal >/dev/null 2>&1; then
fail "platform facade/internal Wi-Fi behavior hooks must not expose semantic prompt string IDs"
fi
if rg -n "VOICE_PROMPT_WIFI_|voice_prompt_id_t|voice_prompt_play\\(" components/platform/internal components/platform/src >/dev/null 2>&1; then
fail "platform must expose voice prompt playback as a primitive file API, not semantic Wi-Fi prompt IDs"
fi
if rg -n "platform_multimodal_downstream_playback_event|PLATFORM_MULTIMODAL_DOWNSTREAM_PLAYBACK" components/platform/include/platform.h >/dev/null 2>&1; then
fail "platform facade must not duplicate domain-owned multimodal semantic events"
fi
if rg -n "esp_afe_sr|afe_config_init|afe_config_check|esp_afe_handle_from_config|SR_WN|MULTIMODAL_WAKE_WORD|wake_word|Wake word|Wake-word|wn9_" \
components/platform/{include,internal,src} \
components/control_plane/{include,internal,src} \
components/app_composition/{include,internal,src} >/dev/null 2>&1 ||
rg -n 'model_path\\.h|esp_srmodel_|esp_mn_' components/domain/{include,internal,src} \
--glob '!domain_local_command.c' >/dev/null 2>&1; then
fail "Local speech models must stay domain-owned and WakeNet/full AFE SR must stay absent"
fi
if ! rg -n 'espressif/esp-sr: \^2\.4\.7' components/platform/idf_component.yml >/dev/null 2>&1 ||
! rg -n 'DOMAIN_LOCAL_COMMAND_MIAO_ZHANG_GUI' \
components/domain/include/domain.h \
components/domain/src/runtime/domain_local_command.c >/dev/null 2>&1 ||
! rg -n 'miao zhang gui' \
components/domain/src/runtime/domain_local_command.c >/dev/null 2>&1; then
fail "Local command detection must keep the ESP-SR dependency and 喵掌柜 domain contract"
fi
if ! rg -n 'TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE' \
main/Kconfig.projbuild \
components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c >/dev/null 2>&1 ||
! rg -n 'TQ_LOCAL_WAKE_READY_TIMEOUT_MS' \
main/Kconfig.projbuild \
sdkconfig.defaults \
components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c >/dev/null 2>&1 ||
! rg -n 'CONFIG_TQ_LOCAL_WAKE_RESPONSE_PROMPT_ENABLE=y' \
sdkconfig.defaults >/dev/null 2>&1 ||
! rg -n 'local_wake_response\.opus' \
main/Kconfig.projbuild \
sdkconfig.defaults \
components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c >/dev/null 2>&1 ||
[[ ! -f components/platform/assets/audio_prompts/local_wake_response.opus ]]; then
fail "Local wake response prompt must remain enabled with its packaged asset contract"
fi
if ! rg -n 'DOMAIN_EXPRESSION_LISTENING|DOMAIN_EXPRESSION_TRIGGER_LOCAL_WAKE' \
components/domain/include/domain.h \
components/domain/src/runtime/domain_runtime_adapters.c \
components/control_plane/src/multimodal/control_plane_multimodal_local_wake.c >/dev/null 2>&1 ||
! rg -n 'PTT_STATE_WAIT_WAKE_PROMPT|control_plane_local_wake_prompt_gate_active' \
components/control_plane/src/runtime/control_plane_runtime_workers.c >/dev/null 2>&1 ||
! rg -n '!local_wake_prompt_gate.*&&' \
components/control_plane/src/runtime/control_plane_runtime_workers.c >/dev/null 2>&1 ||
! rg -n '^ listening[012]$' components/platform/CMakeLists.txt >/dev/null 2>&1 ||
! rg -n 'listening[012]\.png' components/platform/src/lcd_direct_assets.c >/dev/null 2>&1 ||
[[ ! -f components/platform/assets/lcd_optimized/listening0.png ]] ||
[[ ! -f components/platform/assets/lcd_optimized/listening1.png ]] ||
[[ ! -f components/platform/assets/lcd_optimized/listening2.png ]]; then
fail "Local wake must show its packaged listening animation while gating speech uplink until the ready prompt finishes"
fi
if ! rg -n '#include "esp_afe_aec.h"' components/platform/src/multimodal/audio_pipeline.c >/dev/null 2>&1 ||
! rg -n 'afe_aec_create\(' components/platform/src/multimodal/audio_pipeline/audio_pipeline_codec_impl.c >/dev/null 2>&1 ||
! rg -n 'afe_aec_process\(' components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c >/dev/null 2>&1; then
fail "Platform must own the synchronous ESP-SR AEC implementation"
fi
if rg -n '\.disable_auto_reconnect = false|\.enable_close_reconnect = true' \
components/platform/src/multimodal/transport_ws/transport_ws_workers_impl.c >/dev/null 2>&1 ||
! rg -n '\.disable_auto_reconnect = true' \
components/platform/src/multimodal/transport_ws/transport_ws_workers_impl.c >/dev/null 2>&1 ||
! rg -n 'reason == CONTROL_PLANE_TRANSPORT_LOSS_WS_ERROR' \
components/control_plane/src/multimodal/control_plane_multimodal_session_service.c >/dev/null 2>&1 ||
! rg -n 'reason == CONTROL_PLANE_TRANSPORT_LOSS_WS_DISCONNECTED' \
components/control_plane/src/multimodal/control_plane_multimodal_session_service.c >/dev/null 2>&1 ||
! rg -n 'Schedule fresh-auth reconnect after transport (error|disconnect)' \
components/control_plane/src/multimodal/control_plane_multimodal_runtime.c >/dev/null 2>&1 ||
! rg -n 'if \(!ws_started\)' \
components/control_plane/src/runtime/control_plane_runtime_workers.c >/dev/null 2>&1; then
fail "WebSocket recovery must retire stale credentials and reconnect through fresh control-plane authentication"
fi
if rg -n 'esp_afe_aec\.h|afe_aec_process\(' \
components/domain/{include,internal,src} \
components/control_plane/{include,internal,src} \
components/app_composition/{include,internal,src} >/dev/null 2>&1; then
fail "AEC execution must remain a platform primitive"
fi
if ! rg -n '^CONFIG_MULTIMODAL_AUDIO_CODEC_CHANNELS=2\r?$' sdkconfig.defaults >/dev/null 2>&1; then
fail "ES8311 microphone and DAC reference capture requires two physical codec slots"
fi
if rg -n "platform_lcd_trigger_slot_animation|lcd_trigger_slot_animation" components/platform/include components/platform/internal components/platform/src/runtime >/dev/null 2>&1; then
fail "platform must expose LCD presentation primitives, not one-off expression APIs"
fi
if rg -n "control_plane_get_direction_(count|view|current_direction_index)|control_plane_get_current_angle" components/control_plane/include/control_plane.h components/control_plane/src >/dev/null 2>&1; then
fail "control-plane facade/source must not duplicate domain-owned direction catalog query contracts"
fi
if rg -n "control_plane_direction_default_(axis_config|calibration)|CONTROL_PLANE_CAL_" components/control_plane >/dev/null 2>&1; then
fail "direction calibration defaults are domain-owned and must not be duplicated in control-plane"
fi
if rg -n "void\\s+platform_multimodal_(transport_stop|audio_stop_uplink)\\s*\\(|void\\s+domain_runtime_multimodal_(transport_stop|audio_stop_uplink)\\s*\\(" components/platform/include/platform.h components/domain/include/domain.h >/dev/null 2>&1; then
fail "multimodal stop facade APIs must expose timeout/result semantics"
fi
if rg -n "void\\s+(domain_runtime|control_plane)_stop_lcd_render_loop\\s*\\(" components/domain/include/domain.h components/control_plane/include/control_plane.h >/dev/null 2>&1; then
fail "LCD stop facade APIs must expose timeout/result semantics"
fi
echo "[CHECK] component dependency ownership"
if rg -n "^\\s*esp_wifi\\s*$|^\\s*nvs_flash\\s*$" components/domain/CMakeLists.txt >/dev/null 2>&1; then
fail "domain must not depend directly on esp_wifi or nvs_flash"
fi
if rg -n "^\\s*REQUIRES\\s*$" components/control_plane/CMakeLists.txt >/dev/null 2>&1; then
fail "control-plane must not publicly re-export domain include paths; use PRIV_REQUIRES for domain"
fi
if rg -n "#include \"multimodal/(audio_pipeline|transport_ws)\\.h\"" components/domain >/dev/null 2>&1; then
fail "domain must not include platform multimodal internal headers directly"
fi
echo "[CHECK] no cross-component internal includes"
if rg -n "#include \"components/.*/internal/.*\"" components main >/dev/null 2>&1; then
fail "cross-component internal include pattern found"
fi
echo "[CHECK] no unbounded wait in control-plane runtime stop paths"
if rg -n "portMAX_DELAY" components/control_plane/src/runtime >/dev/null 2>&1; then
fail "control-plane runtime paths must avoid unbounded portMAX_DELAY"
fi
if rg -n "CONTROL_PLANE_MODEL_CMD_INTERNAL_STOP|command_type\\s*=\\s*\\(int\\)command_type" components/control_plane/src/runtime/control_plane_runtime_workers.c >/dev/null 2>&1; then
fail "control-plane model command queue must keep typed command contracts and avoid magic sentinels"
fi
if rg -n "FACTORY_CLI_PARK_DELAY_MS|Cooperative exit: park" components/control_plane/src/runtime/control_plane_factory_cli.c >/dev/null 2>&1; then
fail "factory CLI worker must cooperatively exit and clear its handle instead of parking for force-delete"
fi
if rg -n "if \\(s_(model_cmd|ws_wait|lcd_render_loop|wifi_state|ptt|slot_switch)_task == NULL\\)" components/control_plane/src/runtime/control_plane_runtime_workers.c >/dev/null 2>&1; then
fail "control-plane runtime worker task handles must be read through the runtime lock helpers"
fi
if ! rg -n "ws_start_async_stop" components/platform/src/multimodal/transport_ws/transport_ws_runtime_impl.c >/dev/null 2>&1; then
fail "websocket zero-timeout stop must start an asynchronous teardown worker"
fi
if rg -n "effective_timeout_ms = timeout_ms > 0 \\? timeout_ms : 1" components/platform/src/multimodal/transport_ws/transport_ws_runtime_impl.c >/dev/null 2>&1; then
fail "zero-timeout stop must be asynchronous, not a 1ms synchronous teardown"
fi
if perl -0ne 'our $found; $found = 1 if /esp_err_t multimodal_audio_stop_uplink\(int timeout_ms\).*?effective_timeout_ms\s*=\s*timeout_ms\s*>\s*0\s*\?\s*timeout_ms\s*:\s*1/s; END { exit($found ? 0 : 1) }' components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c >/dev/null 2>&1; then
fail "zero-timeout stop must be asynchronous, not a 1ms synchronous teardown"
fi
echo "[CHECK] outbound HTTPS owner boundary"
if rg -n "#include \"esp_http_client.h\"|#include <esp_http_client.h>|esp_http_client_" components/control_plane >/dev/null 2>&1; then
fail "control-plane must not use esp_http_client directly; route outbound HTTPS through the owner adapter"
fi
if rg -n "domain_runtime_storage_" components/control_plane >/dev/null 2>&1; then
fail "control-plane must not use generic storage adapters; use owner-scoped domain ports"
fi
echo "[PASS] architecture guards"