256 lines
7.2 KiB
Bash
Executable File
256 lines
7.2 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)"
|
|
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
|