#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" RESOLVER="${SCRIPT_DIR}/resolve-feature-base" EXPLICIT_REF="" if [[ $# -gt 0 && "$1" != --* ]]; then EXPLICIT_REF="$1" shift fi if [[ ! -x "${RESOLVER}" ]]; then echo "UI/Productization Coverage guard: executable resolver is unavailable at ${RESOLVER}." >&2 exit 2 fi resolver_arguments=(--format=kv) if [[ -n "${EXPLICIT_REF}" ]]; then resolver_arguments+=("--ref=${EXPLICIT_REF}") fi resolver_arguments+=("$@") set +e RESOLVER_OUTPUT="$( cd "${ROOT_DIR}" "${RESOLVER}" "${resolver_arguments[@]}" 2>&1 )" RESOLVER_STATUS=$? set -e if [[ ${RESOLVER_STATUS} -ne 0 ]]; then printf '%s\n' "${RESOLVER_OUTPUT}" >&2 exit 2 fi RESOLUTION_STATUS="" BRANCH_FAMILY="" EXPECTED_BRANCH="" SELECTED_REF="" TARGET_COMMIT="" MERGE_BASE="" BASE_RANGE="" while IFS='=' read -r key value; do case "${key}" in status) RESOLUTION_STATUS="${value}" ;; branch_family) BRANCH_FAMILY="${value}" ;; expected_branch) EXPECTED_BRANCH="${value}" ;; selected_ref) SELECTED_REF="${value}" ;; target_commit) TARGET_COMMIT="${value}" ;; merge_base) MERGE_BASE="${value}" ;; base_range) BASE_RANGE="${value}" ;; esac done <<< "${RESOLVER_OUTPUT}" if [[ "${RESOLUTION_STATUS}" != "resolved" \ || -z "${BRANCH_FAMILY}" \ || -z "${EXPECTED_BRANCH}" \ || -z "${SELECTED_REF}" \ || -z "${TARGET_COMMIT}" \ || -z "${MERGE_BASE}" \ || -z "${BASE_RANGE}" ]]; then echo "UI/Productization Coverage guard: resolver returned an incomplete success contract." >&2 printf '%s\n' "${RESOLVER_OUTPUT}" >&2 exit 2 fi printf '%s\n' "${RESOLVER_OUTPUT}" collect_changed_files() { git -C "${ROOT_DIR}" diff --name-only --diff-filter=ACDMRT "${BASE_RANGE}" 2>/dev/null || true git -C "${ROOT_DIR}" diff --cached --name-only --diff-filter=ACDMRT 2>/dev/null || true git -C "${ROOT_DIR}" diff --name-only --diff-filter=ACDMRT 2>/dev/null || true git -C "${ROOT_DIR}" ls-files --others --exclude-standard 2>/dev/null || true } is_ui_surface_path() { case "$1" in apps/platform/app/Filament/*|\ apps/platform/resources/views/*|\ apps/platform/app/Livewire/*|\ apps/platform/routes/*|\ apps/platform/app/Support/Navigation/*|\ apps/platform/app/Providers/Filament/*) return 0 ;; esac return 1 } is_coverage_artifact_path() { case "$1" in docs/ui-ux-enterprise-audit/route-inventory.md|\ docs/ui-ux-enterprise-audit/design-coverage-matrix.md|\ docs/ui-ux-enterprise-audit/grouped-follow-up-candidates.md|\ docs/ui-ux-enterprise-audit/strategic-surfaces.md|\ docs/ui-ux-enterprise-audit/unresolved-pages.md|\ docs/ui-ux-enterprise-audit/page-reports/*) return 0 ;; esac return 1 } has_checked_no_impact_with_rationale() { local file="$1" [[ -f "${ROOT_DIR}/${file}" ]] || return 1 awk ' BEGIN { window = 0 awaiting_block = 0 found = 0 } /^- \[[xX]\] No UI surface impact[[:space:]]*$/ { window = 20 awaiting_block = 0 next } window > 0 { if ($0 ~ /^[[:space:]]*([*-][[:space:]]*)?(\*\*)?(No-impact[[:space:]]+rationale|Rationale)(\*\*)?[[:space:]]*:/) { rationale_line = $0 sub(/^[[:space:]]*([*-][[:space:]]*)?(\*\*)?(No-impact[[:space:]]+rationale|Rationale)(\*\*)?[[:space:]]*:[[:space:]]*/, "", rationale_line) if (rationale_line ~ /[^[:space:]]/) { found = 1 exit } awaiting_block = 4 window-- next } if (awaiting_block > 0) { if ($0 ~ /^[[:space:]]*$/) { awaiting_block-- window-- next } if ($0 ~ /^- \[[ xX]\]/ || $0 ~ /^##/) { awaiting_block = 0 window-- next } found = 1 exit } window-- } END { exit found ? 0 : 1 } ' "${ROOT_DIR}/${file}" } has_checked_ui_impact() { local file="$1" [[ -f "${ROOT_DIR}/${file}" ]] || return 1 grep -Eq '^- \[[xX]\] (Existing page changed|New page/route added|Navigation changed|Filament panel/provider surface changed|New modal/drawer/wizard/action added|New modal/drawer/wizard added|New modal/drawer/action added|New table/form/state added|Customer-facing surface changed|Dangerous action changed|Status/evidence/review presentation changed|Workspace/environment context presentation changed)[[:space:]]*$' "${ROOT_DIR}/${file}" } ui_changes=() coverage_changes=() spec_no_impact_files=() spec_impact_files=() while IFS= read -r file; do [[ -z "${file}" ]] && continue if is_ui_surface_path "${file}"; then ui_changes+=("${file}") fi if is_coverage_artifact_path "${file}"; then coverage_changes+=("${file}") fi if [[ "${file}" == specs/*/spec.md ]]; then if has_checked_ui_impact "${file}"; then spec_impact_files+=("${file}") fi if has_checked_no_impact_with_rationale "${file}"; then spec_no_impact_files+=("${file}") fi fi done < <(collect_changed_files | sort -u) if [[ ${#ui_changes[@]} -eq 0 ]]; then echo "UI/Productization Coverage guard passed: no guarded UI surface paths changed." exit 0 fi if [[ ${#coverage_changes[@]} -gt 0 || ${#spec_impact_files[@]} -gt 0 || ${#spec_no_impact_files[@]} -gt 0 ]]; then echo "UI/Productization Coverage guard passed." echo "Guarded UI surface changes:" printf ' - %s\n' "${ui_changes[@]}" if [[ ${#coverage_changes[@]} -gt 0 ]]; then echo "Coverage artifacts changed:" printf ' - %s\n' "${coverage_changes[@]}" fi if [[ ${#spec_impact_files[@]} -gt 0 ]]; then echo "Checked UI impact decision found in:" printf ' - %s\n' "${spec_impact_files[@]}" fi if [[ ${#spec_no_impact_files[@]} -gt 0 ]]; then echo "Checked no-impact decision with nearby rationale found in:" printf ' - %s\n' "${spec_no_impact_files[@]}" fi exit 0 fi cat >&2 <<'EOF' UI/Productization Coverage guard failed. Guarded UI surface files changed, but no UI coverage artifact changed, no changed spec contains a checked UI impact decision, and no changed spec contains a checked no-impact decision with nearby rationale: - [x] No UI surface impact Required: update at least one relevant artifact under docs/ui-ux-enterprise-audit/ (route inventory, design coverage matrix, page reports, grouped follow-up candidates, strategic surfaces, or unresolved pages), document a checked proportional UI Surface Impact decision in the active spec, or document the checked no-impact decision with a nearby non-empty Rationale / No-impact rationale block in the active spec. Guarded UI surface changes: EOF printf ' - %s\n' "${ui_changes[@]}" >&2 exit 1