From 9016f8dbeb235dc436a547e3f4889b3e19d3a048 Mon Sep 17 00:00:00 2001 From: potter Date: Mon, 11 May 2026 15:36:32 +0800 Subject: [PATCH] Add frontend static boundary guard --- tools/ci/README.md | 1 + tools/ci/architecture_guards.sh | 3 +- tools/ci/frontend_static_boundary_guard.sh | 84 ++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100755 tools/ci/frontend_static_boundary_guard.sh diff --git a/tools/ci/README.md b/tools/ci/README.md index ebc48f8d3..f9080b611 100644 --- a/tools/ci/README.md +++ b/tools/ci/README.md @@ -8,6 +8,7 @@ This directory keeps CI gate scripts and smoke tests. - Produces a filtered `Cobertura.xml` under `artifacts/coverage/-ci-gate/report/` after applying assembly/file exclusions for non-core shells/adapters such as `Aevatar.Tools.*`, `Aevatar.Studio.*`, `Aevatar.Authentication.*`, and host app entrypoints. - `tools/ci/architecture_guards.sh`: architecture/static guards (includes projection route mapping guard). - `tools/ci/channel_mega_interface_guard.sh`: blocks regressions that merge channel runtime and outbound methods back into one mega interface. +- `tools/ci/frontend_static_boundary_guard.sh`: blocks frontend regressions that call actor-state/replay/projection-refresh endpoints, parse actorId prefixes, or depend on internal EventEnvelope routing fields. - `tools/ci/fetch_latest_ci_failure.sh`: downloads the latest failed GitHub Actions run metadata and failed logs into `artifacts/ci-failures/latest/` via `gh`. - `tools/ci/test_stability_guards.sh`: polling/unstable test pattern guard. - `tools/ci/solution_split_guards.sh`: split build guard. diff --git a/tools/ci/architecture_guards.sh b/tools/ci/architecture_guards.sh index 05012e085..b4bf0f788 100755 --- a/tools/ci/architecture_guards.sh +++ b/tools/ci/architecture_guards.sh @@ -92,6 +92,7 @@ bash "${SCRIPT_DIR}/channel_relay_nyx_chat_direct_create_guard.sh" bash "${SCRIPT_DIR}/channel_tombstone_proto_field_guard.sh" bash "${SCRIPT_DIR}/agent_tool_delivery_target_reader_guard.sh" bash "${SCRIPT_DIR}/studio_projection_readmodel_registration_guard.sh" +bash "${SCRIPT_DIR}/frontend_static_boundary_guard.sh" secret_store_scan_roots=() while IFS= read -r host_dir; do @@ -360,7 +361,7 @@ END { exit 1; } } -' +' )" state_direct_mutation_status=$? set -e diff --git a/tools/ci/frontend_static_boundary_guard.sh b/tools/ci/frontend_static_boundary_guard.sh new file mode 100755 index 000000000..8aef9c720 --- /dev/null +++ b/tools/ci/frontend_static_boundary_guard.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# +# Frontend static boundary guard. +# +# Prevents frontend code from violating CQRS / Actor architecture boundaries +# defined in CLAUDE.md and docs/canon/cqrs-projection.md: +# +# 1. Calling actor-state, event-replay, or projection-refresh endpoints +# (queries must go through readmodel, not replay or refresh). +# 2. Parsing actorId string prefixes for business semantics +# (actorId is an opaque address per CLAUDE.md "actorId 对调用方不透明"). +# 3. Accessing EventEnvelope route/runtime/propagation internals +# (envelope internals are delivery context, not business completion semantics). +# +# Failure mode: a frontend PR introduces direct actor-state reads, event-replay +# polling, or actorId prefix parsing; CI fails fast with the offending file. + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" +cd "${REPO_ROOT}" + +FRONTEND_SRC="apps/aevatar-console-web/src" + +if [[ ! -d "${FRONTEND_SRC}" ]]; then + echo "frontend_static_boundary_guard: skipped (${FRONTEND_SRC} does not exist)" + exit 0 +fi + +FRONTEND_EXCLUDES=( + -g '!**/*.test.*' + -g '!**/__tests__/**' + -g '!**/.umi*/**' + -g '!**/node_modules/**' +) + +failures=0 + +forbidden_route_hits="$( + rg -n '(/actor-state|/events/replay|/projections?/refresh)' "${FRONTEND_SRC}" \ + -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' \ + "${FRONTEND_EXCLUDES[@]}" \ + || true +)" + +if [[ -n "${forbidden_route_hits}" ]]; then + echo "${forbidden_route_hits}" + echo "Frontend code must not call actor-state, event replay, or projection refresh endpoints." + failures=1 +fi + +actor_id_parsing_hits="$( + rg -n -P '\b[\w$]*[Aa]ctorId\b\s*\??\s*\.\s*(startsWith|includes|indexOf|match|split)\s*\(' "${FRONTEND_SRC}" \ + -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' \ + "${FRONTEND_EXCLUDES[@]}" \ + || true +)" + +if [[ -n "${actor_id_parsing_hits}" ]]; then + echo "${actor_id_parsing_hits}" + echo "Frontend code must treat actorId as an opaque address, not parse it for business semantics." + failures=1 +fi + +event_envelope_field_hits="$( + rg -n -P '\b(eventEnvelope|actorEnvelope|runtimeEnvelope|workflowRunEventEnvelope)\b\s*\??\s*\.\s*(route|runtime|propagation)\b' "${FRONTEND_SRC}" \ + -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' \ + "${FRONTEND_EXCLUDES[@]}" \ + || true +)" + +if [[ -n "${event_envelope_field_hits}" ]]; then + echo "${event_envelope_field_hits}" + echo "Frontend code must not branch on EventEnvelope route/runtime/propagation internals." + failures=1 +fi + +if [[ "${failures}" -ne 0 ]]; then + echo "frontend_static_boundary_guard: failed" + exit 1 +fi + +echo "frontend_static_boundary_guard: ok"