#!/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/listening0.png ]] || [[ ! -f components/platform/assets/lcd/listening1.png ]] || [[ ! -f components/platform/assets/lcd/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_" 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"