TenantAtlas/scripts/check-ui-productization-coverage
ahmido e35706b846 Spec 324: add UI productization coverage guardrails (#384)
## Summary
- add the Spec 324 package for UI Productization Coverage Guardrails, including spec, plan, tasks, and requirements checklist
- update Spec Kit templates and implementation prompts so future work must record UI surface impact, including navigation and Filament panel/provider surfaces
- harden the UI productization coverage guard script and add the validation helper for lightweight guard execution
- document the proportional guardrail flow in the UI/UX enterprise audit README

## Validation
- not run in this step
- change set is docs/tooling/governance only; no product runtime implementation included

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #384
2026-05-17 19:01:48 +00:00

214 lines
6.5 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
BASE_REF="${1:-${GITHUB_BASE_REF:-${GITEA_BASE_REF:-}}}"
if [[ -z "${BASE_REF}" ]]; then
if git -C "${ROOT_DIR}" rev-parse --verify origin/dev >/dev/null 2>&1; then
BASE_REF="origin/dev"
elif git -C "${ROOT_DIR}" rev-parse --verify HEAD~1 >/dev/null 2>&1; then
BASE_REF="HEAD~1"
else
echo "UI/Productization Coverage guard: no base ref supplied and no fallback ref exists." >&2
echo "Usage: scripts/check-ui-productization-coverage <base-ref>" >&2
exit 2
fi
fi
if ! git -C "${ROOT_DIR}" rev-parse --verify "${BASE_REF}^{commit}" >/dev/null 2>&1; then
echo "UI/Productization Coverage guard: base ref '${BASE_REF}' is not available." >&2
echo "Fetch the target branch first or pass an available commit/ref." >&2
exit 2
fi
if BASE_COMMIT="$(git -C "${ROOT_DIR}" merge-base "${BASE_REF}" HEAD 2>/dev/null)"; then
BASE_RANGE="${BASE_COMMIT}...HEAD"
else
BASE_RANGE="${BASE_REF}..HEAD"
fi
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